robodev 0.47.1 → 0.47.2
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/package.json +1 -1
- package/src/config.test.ts +99 -1
- package/src/config.ts +17 -0
- package/src/index.ts +59 -26
- package/src/resolve-deploy-auth.test.ts +65 -0
- package/src/resolve-deploy-auth.ts +31 -0
- package/templates/auth-chat/ROBODEV.md +4 -0
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/ROBODEV.md +4 -0
- package/templates/backend/package.json +1 -1
- package/templates/empty/README.md +1 -1
- package/templates/empty/ROBODEV.md +4 -0
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/ROBODEV.md +4 -0
- package/templates/marketplace/package.json +1 -1
- package/templates/shadcn/README.md +1 -1
- package/templates/shadcn/ROBODEV.md +4 -0
- package/templates/shadcn/package.json +1 -1
- package/templates/space/ROBODEV.md +4 -0
- package/templates/space/package.json +1 -1
package/package.json
CHANGED
package/src/config.test.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
defaultFrontendConfig,
|
|
8
|
+
readLink,
|
|
9
|
+
resolveDeployKey,
|
|
10
|
+
writeLink,
|
|
11
|
+
writeViteApiUrl,
|
|
12
|
+
} from "./config.js";
|
|
7
13
|
|
|
8
14
|
test("writeViteApiUrl sets Vite and Povio public API URLs", async () => {
|
|
9
15
|
const root = await mkdtemp(join(tmpdir(), "rd-env-"));
|
|
@@ -159,6 +165,98 @@ test("writeLink preserves frontend false opt-out", async () => {
|
|
|
159
165
|
}
|
|
160
166
|
});
|
|
161
167
|
|
|
168
|
+
test("writeLink keeps an existing deployToken when rewriting other fields", async () => {
|
|
169
|
+
const root = await mkdtemp(join(tmpdir(), "rd-link-token-"));
|
|
170
|
+
try {
|
|
171
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
172
|
+
await writeFile(
|
|
173
|
+
join(root, "robodev/.robodev"),
|
|
174
|
+
`${JSON.stringify(
|
|
175
|
+
{
|
|
176
|
+
projectId: "prj-1",
|
|
177
|
+
apiUrl: "https://a.example",
|
|
178
|
+
projectApiUrl: "https://prj-1.example",
|
|
179
|
+
deployToken: "rdb_keep",
|
|
180
|
+
},
|
|
181
|
+
null,
|
|
182
|
+
2,
|
|
183
|
+
)}\n`,
|
|
184
|
+
);
|
|
185
|
+
await writeLink({ projectId: "prj-1", apiUrl: "https://b.example" }, root);
|
|
186
|
+
const raw = JSON.parse(await readFile(join(root, "robodev/.robodev"), "utf8")) as {
|
|
187
|
+
deployToken?: string;
|
|
188
|
+
apiUrl: string;
|
|
189
|
+
};
|
|
190
|
+
assert.equal(raw.deployToken, "rdb_keep");
|
|
191
|
+
assert.equal(raw.apiUrl, "https://b.example");
|
|
192
|
+
const link = await readLink(root);
|
|
193
|
+
assert.equal(link && "deployToken" in link, false);
|
|
194
|
+
} finally {
|
|
195
|
+
await rm(root, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("resolveDeployKey prefers ROBODEV_DEPLOY_KEY over a file token", async () => {
|
|
200
|
+
const root = await mkdtemp(join(tmpdir(), "rd-key-env-"));
|
|
201
|
+
const previous = process.env.ROBODEV_DEPLOY_KEY;
|
|
202
|
+
try {
|
|
203
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
204
|
+
await writeFile(
|
|
205
|
+
join(root, "robodev/.robodev"),
|
|
206
|
+
`${JSON.stringify({ projectId: "prj-1", apiUrl: "https://a.example", deployToken: "rdb_file" }, null, 2)}\n`,
|
|
207
|
+
);
|
|
208
|
+
process.env.ROBODEV_DEPLOY_KEY = " rdb_env ";
|
|
209
|
+
assert.equal(await resolveDeployKey(root), "rdb_env");
|
|
210
|
+
} finally {
|
|
211
|
+
if (previous === undefined) delete process.env.ROBODEV_DEPLOY_KEY;
|
|
212
|
+
else process.env.ROBODEV_DEPLOY_KEY = previous;
|
|
213
|
+
await rm(root, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("resolveDeployKey reads deployToken from robodev/.robodev then legacy .robodev", async () => {
|
|
218
|
+
const root = await mkdtemp(join(tmpdir(), "rd-key-file-"));
|
|
219
|
+
const previous = process.env.ROBODEV_DEPLOY_KEY;
|
|
220
|
+
try {
|
|
221
|
+
delete process.env.ROBODEV_DEPLOY_KEY;
|
|
222
|
+
await writeFile(
|
|
223
|
+
join(root, ".robodev"),
|
|
224
|
+
`${JSON.stringify({ projectId: "legacy", apiUrl: "https://legacy.example", deployToken: "rdb_legacy" }, null, 2)}\n`,
|
|
225
|
+
);
|
|
226
|
+
assert.equal(await resolveDeployKey(root), "rdb_legacy");
|
|
227
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
228
|
+
await writeFile(
|
|
229
|
+
join(root, "robodev/.robodev"),
|
|
230
|
+
`${JSON.stringify({ projectId: "canonical", apiUrl: "https://canonical.example", deployToken: "rdb_canonical" }, null, 2)}\n`,
|
|
231
|
+
);
|
|
232
|
+
assert.equal(await resolveDeployKey(root), "rdb_canonical");
|
|
233
|
+
} finally {
|
|
234
|
+
if (previous === undefined) delete process.env.ROBODEV_DEPLOY_KEY;
|
|
235
|
+
else process.env.ROBODEV_DEPLOY_KEY = previous;
|
|
236
|
+
await rm(root, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("resolveDeployKey ignores blank env and file tokens", async () => {
|
|
241
|
+
const root = await mkdtemp(join(tmpdir(), "rd-key-blank-"));
|
|
242
|
+
const previous = process.env.ROBODEV_DEPLOY_KEY;
|
|
243
|
+
try {
|
|
244
|
+
process.env.ROBODEV_DEPLOY_KEY = " ";
|
|
245
|
+
await mkdir(join(root, "robodev"), { recursive: true });
|
|
246
|
+
await writeFile(
|
|
247
|
+
join(root, "robodev/.robodev"),
|
|
248
|
+
`${JSON.stringify({ projectId: "prj-1", apiUrl: "https://a.example", deployToken: " " }, null, 2)}\n`,
|
|
249
|
+
);
|
|
250
|
+
assert.equal(await resolveDeployKey(root), null);
|
|
251
|
+
delete process.env.ROBODEV_DEPLOY_KEY;
|
|
252
|
+
assert.equal(await resolveDeployKey(root), null);
|
|
253
|
+
} finally {
|
|
254
|
+
if (previous === undefined) delete process.env.ROBODEV_DEPLOY_KEY;
|
|
255
|
+
else process.env.ROBODEV_DEPLOY_KEY = previous;
|
|
256
|
+
await rm(root, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
162
260
|
test("readLink prefers robodev/.robodev over a legacy root file", async () => {
|
|
163
261
|
const root = await mkdtemp(join(tmpdir(), "rd-link-pref-"));
|
|
164
262
|
try {
|
package/src/config.ts
CHANGED
|
@@ -145,6 +145,23 @@ export async function readLink(cwd = process.cwd()): Promise<ProjectLink | null>
|
|
|
145
145
|
return link;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function trimNonEmpty(value: unknown): string | null {
|
|
149
|
+
if (typeof value !== "string") return null;
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Headless deploy credential. `ROBODEV_DEPLOY_KEY` wins over `deployToken` in the link file.
|
|
156
|
+
* Does not read or write `~/.robodev/credentials.json`. Never persist the env value.
|
|
157
|
+
*/
|
|
158
|
+
export async function resolveDeployKey(cwd = process.cwd()): Promise<string | null> {
|
|
159
|
+
const fromEnv = trimNonEmpty(process.env.ROBODEV_DEPLOY_KEY);
|
|
160
|
+
if (fromEnv) return fromEnv;
|
|
161
|
+
const raw = await readLinkJson(cwd);
|
|
162
|
+
return trimNonEmpty(raw?.deployToken);
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
export async function writeLink(link: ProjectLink, cwd = process.cwd()): Promise<void> {
|
|
149
166
|
const existing = (await readLinkJson(cwd)) ?? {};
|
|
150
167
|
const next: Record<string, unknown> = {
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { stdin, stdout } from "node:process";
|
|
|
6
6
|
import { basename, join, relative, resolve } from "node:path";
|
|
7
7
|
import { execFile, spawn } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
-
import { ApiError, authed, publicRequest } from "./api.js";
|
|
9
|
+
import { ApiError, authed, deployKeyRequest, publicRequest } from "./api.js";
|
|
10
10
|
import {
|
|
11
11
|
clearCredentials,
|
|
12
12
|
defaultFrontendConfig,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ProjectLink,
|
|
17
17
|
readLink,
|
|
18
18
|
readLinkJson,
|
|
19
|
+
resolveDeployKey,
|
|
19
20
|
starbaseUrl,
|
|
20
21
|
writeCredentials,
|
|
21
22
|
writeLink,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
import { parseCreateArgs } from "./parse-create-args.js";
|
|
46
47
|
import { parseDeployArgs } from "./parse-deploy-args.js";
|
|
47
48
|
import { ensureDeployLink } from "./ensure-deploy-link.js";
|
|
49
|
+
import { DEPLOY_NOT_AUTHENTICATED_MESSAGE, resolveDeployAuth } from "./resolve-deploy-auth.js";
|
|
48
50
|
import { runLoad } from "./load.js";
|
|
49
51
|
import { runMcp } from "./mcp.js";
|
|
50
52
|
import { printLiveUrl } from "./print-live-url.js";
|
|
@@ -63,7 +65,7 @@ Commands:
|
|
|
63
65
|
projects List your projects
|
|
64
66
|
create [path] [--template <id>] [--starter <id>] [--empty] [--org <id>] Create a project, scaffold a starter, and deploy
|
|
65
67
|
link [projectId] Write robodev/.robodev in the current folder
|
|
66
|
-
deploy [--force] [--name <name>] [--org <id>] Deploy schema, APIs, jobs, and frontend (
|
|
68
|
+
deploy [--force] [--name <name>] [--org <id>] Deploy schema, APIs, jobs, and frontend (JWT, ROBODEV_DEPLOY_KEY, or file deployToken)
|
|
67
69
|
load Download the project source onto this folder
|
|
68
70
|
dev [options] Run this project's APIs locally on an embedded Postgres, no Starbase needed
|
|
69
71
|
mcp [--project <id>] Serve this project's backend over MCP on stdio, for an external coding agent
|
|
@@ -236,6 +238,8 @@ type DeployOptions = {
|
|
|
236
238
|
root?: string;
|
|
237
239
|
frontendFiles?: CollectedFile[];
|
|
238
240
|
skipFrontendBuild?: boolean;
|
|
241
|
+
/** `robodev create` stays JWT-only even if a leftover CI key is in the environment. */
|
|
242
|
+
auth?: "auto" | "jwt";
|
|
239
243
|
};
|
|
240
244
|
|
|
241
245
|
async function collectDeployPayload(
|
|
@@ -266,6 +270,21 @@ async function collectDeployPayload(
|
|
|
266
270
|
return { files: mergeCollectedFiles(host, frontendFiles), source };
|
|
267
271
|
}
|
|
268
272
|
|
|
273
|
+
async function deployAuthed<T>(path: string, init?: RequestInit): Promise<T> {
|
|
274
|
+
try {
|
|
275
|
+
return await authed<T>(path, init);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof Error && error.message.startsWith("Not authenticated.")) {
|
|
278
|
+
throw new Error(DEPLOY_NOT_AUTHENTICATED_MESSAGE);
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function ensureJwtForDeploy(): Promise<void> {
|
|
285
|
+
await deployAuthed("/v1/auth/me");
|
|
286
|
+
}
|
|
287
|
+
|
|
269
288
|
/** Uploads schema, APIs, and frontend. Destructive schema changes need confirmation or `--force`. */
|
|
270
289
|
async function runDeploy(
|
|
271
290
|
forceFlag: boolean,
|
|
@@ -273,34 +292,43 @@ async function runDeploy(
|
|
|
273
292
|
flags: { name?: string; orgId?: string } = {},
|
|
274
293
|
): Promise<string | undefined> {
|
|
275
294
|
const root = options.root ?? process.cwd();
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
readLink
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
const auth =
|
|
296
|
+
options.auth === "jwt"
|
|
297
|
+
? { kind: "jwt" as const }
|
|
298
|
+
: await resolveDeployAuth(root, { resolveDeployKey, readLink });
|
|
299
|
+
const link =
|
|
300
|
+
auth.kind === "key"
|
|
301
|
+
? auth.link
|
|
302
|
+
: await ensureDeployLink(
|
|
303
|
+
{ root, name: flags.name, orgId: flags.orgId, isTty: Boolean(stdin.isTTY) },
|
|
304
|
+
{
|
|
305
|
+
readLink,
|
|
306
|
+
collectFiles,
|
|
307
|
+
ensureAuth: ensureJwtForDeploy,
|
|
308
|
+
resolveOrganization,
|
|
309
|
+
createProject: (body) =>
|
|
310
|
+
authed<Project>("/v1/projects", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify(body),
|
|
313
|
+
}),
|
|
314
|
+
writeLink,
|
|
315
|
+
writeViteApiUrl,
|
|
316
|
+
hasAppsFePackageJson,
|
|
317
|
+
starbaseUrl,
|
|
318
|
+
projectNameFromPath,
|
|
319
|
+
promptName: (defaultName) => prompt(`Project name [${defaultName}]: `),
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
const request =
|
|
323
|
+
auth.kind === "key"
|
|
324
|
+
? <T>(path: string, init?: RequestInit) => deployKeyRequest<T>(path, auth.key, init)
|
|
325
|
+
: deployAuthed;
|
|
298
326
|
const { files, source } = await collectDeployPayload(root, link, options);
|
|
299
327
|
let force = forceFlag;
|
|
300
328
|
|
|
301
329
|
for (;;) {
|
|
302
330
|
try {
|
|
303
|
-
const result = await
|
|
331
|
+
const result = await request<{
|
|
304
332
|
deploymentId: string;
|
|
305
333
|
databaseName: string;
|
|
306
334
|
databaseCreated?: boolean;
|
|
@@ -637,7 +665,12 @@ async function runCreate(args: string[]): Promise<void> {
|
|
|
637
665
|
skipFrontendBuild = true;
|
|
638
666
|
}
|
|
639
667
|
}
|
|
640
|
-
const liveUrl = await runDeploy(false, {
|
|
668
|
+
const liveUrl = await runDeploy(false, {
|
|
669
|
+
root,
|
|
670
|
+
frontendFiles,
|
|
671
|
+
skipFrontendBuild,
|
|
672
|
+
auth: "jwt",
|
|
673
|
+
});
|
|
641
674
|
|
|
642
675
|
try {
|
|
643
676
|
await pnpmInstall(root);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import type { ProjectLink } from "./config.js";
|
|
4
|
+
import {
|
|
5
|
+
DEPLOY_NOT_AUTHENTICATED_MESSAGE,
|
|
6
|
+
resolveDeployAuth,
|
|
7
|
+
UNLINKED_DEPLOY_KEY_MESSAGE,
|
|
8
|
+
} from "./resolve-deploy-auth.js";
|
|
9
|
+
|
|
10
|
+
const link: ProjectLink = {
|
|
11
|
+
projectId: "proj_1",
|
|
12
|
+
apiUrl: "https://robodev.povio.dev",
|
|
13
|
+
projectApiUrl: "https://proj_1.robodev.povio.dev",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test("resolveDeployAuth uses a key and existing link without creating", async () => {
|
|
17
|
+
const auth = await resolveDeployAuth("/tmp/app", {
|
|
18
|
+
resolveDeployKey: async () => "rdb_secret",
|
|
19
|
+
readLink: async () => link,
|
|
20
|
+
});
|
|
21
|
+
assert.deepEqual(auth, { kind: "key", key: "rdb_secret", link });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("resolveDeployAuth refuses an unlinked key deploy", async () => {
|
|
25
|
+
await assert.rejects(
|
|
26
|
+
() =>
|
|
27
|
+
resolveDeployAuth("/tmp/unlinked", {
|
|
28
|
+
resolveDeployKey: async () => "rdb_secret",
|
|
29
|
+
readLink: async () => null,
|
|
30
|
+
}),
|
|
31
|
+
(error: unknown) => {
|
|
32
|
+
assert.ok(error instanceof Error);
|
|
33
|
+
assert.equal(error.message, UNLINKED_DEPLOY_KEY_MESSAGE);
|
|
34
|
+
assert.match(error.message, /cannot create projects/);
|
|
35
|
+
assert.match(error.message, /robodev\/\.robodev/);
|
|
36
|
+
return true;
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolveDeployAuth falls through to JWT when no key is resolved", async () => {
|
|
42
|
+
const auth = await resolveDeployAuth("/tmp/app", {
|
|
43
|
+
resolveDeployKey: async () => null,
|
|
44
|
+
readLink: async () => null,
|
|
45
|
+
});
|
|
46
|
+
assert.deepEqual(auth, { kind: "jwt" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("resolveDeployAuth prefers a key over an existing link that a JWT could also use", async () => {
|
|
50
|
+
const auth = await resolveDeployAuth("/tmp/app", {
|
|
51
|
+
resolveDeployKey: async () => "rdb_env",
|
|
52
|
+
readLink: async () => link,
|
|
53
|
+
});
|
|
54
|
+
assert.equal(auth.kind, "key");
|
|
55
|
+
if (auth.kind === "key") {
|
|
56
|
+
assert.equal(auth.key, "rdb_env");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("deploy not-authenticated copy mentions auth, env, and file token — not load", () => {
|
|
61
|
+
assert.match(DEPLOY_NOT_AUTHENTICATED_MESSAGE, /robodev auth/);
|
|
62
|
+
assert.match(DEPLOY_NOT_AUTHENTICATED_MESSAGE, /ROBODEV_DEPLOY_KEY/);
|
|
63
|
+
assert.match(DEPLOY_NOT_AUTHENTICATED_MESSAGE, /deployToken/);
|
|
64
|
+
assert.equal(DEPLOY_NOT_AUTHENTICATED_MESSAGE.includes("load"), false);
|
|
65
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ProjectLink } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export const UNLINKED_DEPLOY_KEY_MESSAGE =
|
|
4
|
+
"A deploy key cannot create projects. Link first (`robodev auth` + `robodev link` / `create`) or provide robodev/.robodev with projectId and apiUrl (no token required when ROBODEV_DEPLOY_KEY is set).";
|
|
5
|
+
|
|
6
|
+
export const DEPLOY_NOT_AUTHENTICATED_MESSAGE =
|
|
7
|
+
"Not authenticated. Run `robodev auth`, set ROBODEV_DEPLOY_KEY, or add deployToken to robodev/.robodev.";
|
|
8
|
+
|
|
9
|
+
export type DeployAuth = { kind: "key"; key: string; link: ProjectLink } | { kind: "jwt" };
|
|
10
|
+
|
|
11
|
+
export type ResolveDeployAuthDeps = {
|
|
12
|
+
resolveDeployKey: (cwd: string) => Promise<string | null>;
|
|
13
|
+
readLink: (cwd: string) => Promise<ProjectLink | null>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Key deploy (env or file token) requires an existing link and never creates a project.
|
|
18
|
+
* No key → JWT path, including unlinked create-and-link.
|
|
19
|
+
*/
|
|
20
|
+
export async function resolveDeployAuth(
|
|
21
|
+
root: string,
|
|
22
|
+
deps: ResolveDeployAuthDeps,
|
|
23
|
+
): Promise<DeployAuth> {
|
|
24
|
+
const key = await deps.resolveDeployKey(root);
|
|
25
|
+
if (!key) return { kind: "jwt" };
|
|
26
|
+
const link = await deps.readLink(root);
|
|
27
|
+
if (!link) {
|
|
28
|
+
throw new Error(UNLINKED_DEPLOY_KEY_MESSAGE);
|
|
29
|
+
}
|
|
30
|
+
return { kind: "key", key, link };
|
|
31
|
+
}
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Blank `robodev/database.ts` and `GET /api/health` plus a hosted Welcome page. No sample schema.
|
|
4
4
|
|
|
5
|
-
The project App URL serves the hosted Welcome page. Edit `index.html` and `src/`, then `robodev deploy` (or Code + Save). Agents: read `ROBODEV.md`.
|
|
5
|
+
The project App URL serves the hosted Welcome page. Edit `index.html` and `src/`, then `robodev deploy` (or Code + Save). Agents: read `ROBODEV.md`. Unless the product is a public page, add login/register via reserved `/api/user/auth/*` and protect routes.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
robodev create my-app
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Blank `robodev/database.ts` and `GET /api/health` plus a hosted Welcome page with shadcn and an orval client. No sample schema.
|
|
4
4
|
|
|
5
|
-
The project App URL serves the hosted Welcome page (`/` via React Router in `src/pages/Home.tsx`). Edit `index.html` and `src/`, then `robodev deploy` (or Code + Save). Agents: read `ROBODEV.md`.
|
|
5
|
+
The project App URL serves the hosted Welcome page (`/` via React Router in `src/pages/Home.tsx`). Edit `index.html` and `src/`, then `robodev deploy` (or Code + Save). Agents: read `ROBODEV.md`. Unless the product is a public page, add login/register via reserved `/api/user/auth/*` and protect routes.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
robodev create my-app --template shadcn
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|
|
@@ -32,6 +32,10 @@ export const get = defineApi({
|
|
|
32
32
|
- Publish backend after a batch of file edits, not after every file.
|
|
33
33
|
- Do not publish the frontend.
|
|
34
34
|
|
|
35
|
+
## Auth and product data
|
|
36
|
+
|
|
37
|
+
Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
|
|
38
|
+
|
|
35
39
|
## Backend
|
|
36
40
|
|
|
37
41
|
- Auth: robodev-auth get / set / users / users-delete. Do not invent control-plane HTTP.
|