Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 3.0.0

- Vendor the hard-cut OpenCloud browser SDK 2.0.0 and final schema-2 manifest
contract. Legacy SDK surfaces and `requiredSecrets` are rejected instead of
translated.
- Replace raw-path development fixture writes with SDK-shaped table actions:
`create`, `createMany`, `updateById`, and `deleteById`.
- Make secret intent declarative through `generated`, `required`, and
`optional` manifest modes. Generated values are provisioned automatically;
the CLI exposes only explicit `secret rotate` and secure `secret configure`
workflows.
- Generate new projects with the stable singleton browser SDK and an empty
declarative secret map.

## 2.0.0

- Make manifest schema 2 the only application contract. Projects now use
Expand Down
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ offline source bundle, but cannot connect to or deploy through OpenCloud.

## Install a pinned release

OpenCloud application skills pin an exact CLI release. To install `v2.0.0` in
OpenCloud application skills pin an exact CLI release. To install `v3.0.0` in
an isolated task directory:

```bash
OPENCLOUD_CLI_VERSION="v2.0.0"
OPENCLOUD_CLI_PACKAGE="opencloud-cli-2.0.0.tgz"
OPENCLOUD_CLI_VERSION="v3.0.0"
OPENCLOUD_CLI_PACKAGE="opencloud-cli-3.0.0.tgz"
OPENCLOUD_CLI_DIR="$(mktemp -d)"

curl -fsSLo "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE" \
Expand Down Expand Up @@ -128,15 +128,26 @@ If the email remains unverified when that window ends, OpenCloud pauses every
linked app, function, and cron schedule while preserving data and releases.
Email verification resumes them.

Secrets never need to cross the terminal transcript:
Declare secret intent in `opencloud.yaml`; values never cross the terminal
transcript:

```yaml
secrets:
SESSION_KEY: generated
PAYMENT_API_KEY: required
ORGANIZATION_LABEL: optional
```

Generated values are provisioned automatically. Use these commands only to
rotate a generated value or securely configure a required/optional value:

```bash
"$OPENCLOUD_CLI" secret generate "$APP_ID" SESSION_KEY
"$OPENCLOUD_CLI" secret entry-link "$APP_ID" PAYMENT_API_KEY
"$OPENCLOUD_CLI" secret rotate "$APP_ID" SESSION_KEY
"$OPENCLOUD_CLI" secret configure "$APP_ID" PAYMENT_API_KEY
```

The first command creates a server-generated value. The second returns a
one-time browser URL where the user enters a value directly into OpenCloud.
Rotation never returns the generated value. Configuration returns a one-time
browser URL where the owner enters a value directly into OpenCloud.

Existing installations can still supply `OPENCLOUD_API_URL` and
`OPENCLOUD_TOKEN` explicitly.
Expand All @@ -152,8 +163,10 @@ Use the stable capability preview and isolated migration-replayed database befor
"$OPENCLOUD_CLI" app dev start .
"$OPENCLOUD_CLI" app dev sync .
"$OPENCLOUD_CLI" app dev request . /
"$OPENCLOUD_CLI" app dev data . /rest/v1/items \
--method POST --body '[{"title":"Preview item"}]'
"$OPENCLOUD_CLI" app dev data . items create \
--values '{"title":"Preview item"}'
"$OPENCLOUD_CLI" app dev data . items updateById \
--id "$ITEM_ID" --values '{"title":"Updated preview item"}'
"$OPENCLOUD_CLI" app dev invoke . function-name --body '{"example":true}'
"$OPENCLOUD_CLI" app dev requests .
"$OPENCLOUD_CLI" app dev verify .
Expand All @@ -163,8 +176,10 @@ Use the stable capability preview and isolated migration-replayed database befor
```

Development data is isolated from production and uses dummy records. Auth,
Files, and Functions are available; Realtime, cron, and production secrets are
not. Functions
Files, and Functions are available; Realtime and cron are not. Manifest-
generated secrets receive isolated synthetic development values, while owner-
configured required values remain unavailable and optional values may be
absent. Functions
imported from `@opencloud/server` remain dormant until `app dev invoke` or a
deliberate preview interaction calls them. Exact-revision verification requires
every declared Function to have a successful explicit invocation.
Expand Down Expand Up @@ -205,7 +220,7 @@ app-declared interaction contract on the server:
"$OPENCLOUD_CLI" app verify "$APP_ID"
```

CLI v2 has one release-verification command. The former local smoke, Chromium,
CLI v3 has one release-verification command. The former local smoke, Chromium,
session, and verification-contract commands were removed so agents cannot
mistake a partial diagnostic for the authoritative gate.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencloud/cli",
"version": "2.0.0",
"version": "3.0.0",
"description": "Versioned command-line client for building, deploying, and verifying OpenCloud applications",
"type": "module",
"bin": {
Expand Down
8 changes: 4 additions & 4 deletions src/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ functions:
expect(first.manifest.migrations[0]?.sha256).toMatch(/^[a-f0-9]{64}$/);
expect(first.manifest.runtime).toEqual({
sdk: {
version: "1.0.0",
version: "2.0.0",
},
});
expect(first.files).toEqual([
Expand Down Expand Up @@ -199,7 +199,7 @@ functions: []
expect(bundle.files).not.toContain("functions/forgotten/index.ts");
});

it("preserves an explicit SDK pin instead of replacing it with current", async () => {
it("accepts the explicit installed SDK pin", async () => {
const root = await temporaryDirectory();
await mkdir(path.join(root, "frontend"));
await writeFile(path.join(root, "frontend", "index.html"), "hello");
Expand All @@ -210,14 +210,14 @@ frontend:
directory: frontend
runtime:
sdk:
version: 9.8.7
version: 2.0.0
`,
);

const bundle = await buildBundle(root);
expect(bundle.manifest.runtime).toEqual({
sdk: {
version: "9.8.7",
version: "2.0.0",
},
});
});
Expand Down
56 changes: 56 additions & 0 deletions src/dev-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { devDataRequest } from "./dev-data.js";

describe("development fixture data", () => {
it.each([
[
"create",
{ values: '{"title":"One"}' },
{ path: "/rest/v1/items", method: "POST", body: { title: "One" } },
],
[
"createMany",
{ values: '[{"title":"One"},{"title":"Two"}]' },
{
path: "/rest/v1/items",
method: "POST",
body: [{ title: "One" }, { title: "Two" }],
},
],
[
"updateById",
{ id: "row/1", values: '{"title":"Changed"}' },
{
path: "/rest/v1/items?id=eq.row%2F1",
method: "PATCH",
body: { title: "Changed" },
},
],
[
"deleteById",
{ id: "row-1" },
{ path: "/rest/v1/items?id=eq.row-1", method: "DELETE" },
],
] as const)("maps %s without exposing REST", (action, options, expected) => {
expect(devDataRequest("items", action, options)).toEqual(expected);
});

it("rejects incomplete, malformed, and extraneous inputs", () => {
expect(() => devDataRequest("Items", "create", { values: "{}" })).toThrow(
/lowercase SQL identifier/,
);
expect(() => devDataRequest("items", "create", {})).toThrow();
expect(() =>
devDataRequest("items", "createMany", { values: "{}" }),
).toThrow();
expect(() =>
devDataRequest("items", "deleteById", { id: "row-1", values: "{}" }),
).toThrow();
expect(() =>
devDataRequest("items", "updateById", {
id: "row-1",
values: "not-json",
}),
).toThrow("--values must be valid JSON");
});
});
98 changes: 98 additions & 0 deletions src/dev-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { z } from "zod";

const values = z.record(z.string(), z.unknown());
const table = z.string().regex(/^[a-z_][a-z0-9_]{0,62}$/, {
message: "table must be a lowercase SQL identifier",
});

const actionSchema = z.discriminatedUnion("action", [
z
.object({
table,
action: z.literal("create"),
values,
})
.strict(),
z
.object({
table,
action: z.literal("createMany"),
values: z.array(values).min(1).max(100),
})
.strict(),
z
.object({
table,
action: z.literal("updateById"),
id: z.string().min(1).max(512),
values,
})
.strict(),
z
.object({
table,
action: z.literal("deleteById"),
id: z.string().min(1).max(512),
})
.strict(),
]);

export type DevDataAction =
| "create"
| "createMany"
| "updateById"
| "deleteById";

interface DevDataOptions {
id?: string | undefined;
values?: string | undefined;
}

function parseValues(value: string | undefined): unknown {
if (value === undefined) return undefined;
try {
return JSON.parse(value) as unknown;
} catch {
throw new Error("--values must be valid JSON");
}
}

export function devDataRequest(
tableName: string,
action: DevDataAction,
options: DevDataOptions,
): {
path: string;
method: "POST" | "PATCH" | "DELETE";
body?: unknown;
} {
const parsed = actionSchema.safeParse({
table: tableName,
action,
...(options.id === undefined ? {} : { id: options.id }),
...(options.values === undefined
? {}
: { values: parseValues(options.values) }),
});
if (!parsed.success) {
const issue = parsed.error.issues[0];
const location = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
throw new Error(
`Invalid ${action} fixture${location}: ${issue?.message ?? "invalid input"}`,
);
}
const input = parsed.data;
const suffix =
"id" in input ? `?id=eq.${encodeURIComponent(input.id)}` : "";
const method =
input.action === "updateById"
? "PATCH"
: input.action === "deleteById"
? "DELETE"
: "POST";
return {
path: `/rest/v1/${input.table}${suffix}`,
method,
...(input.action === "deleteById" ? {} : { body: input.values }),
};
}
Loading
Loading