bsmnt 0.5.0 → 0.6.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/README.md +1 -0
- package/dist/application/add-hooks/index.d.ts.map +1 -1
- package/dist/application/add-hooks/index.js +1 -0
- package/dist/application/add-hooks/index.js.map +1 -1
- package/dist/application/add-integration/index.d.ts +13 -6
- package/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +145 -62
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/application/create-project/build-context.d.ts +3 -1
- package/dist/application/create-project/build-context.d.ts.map +1 -1
- package/dist/application/create-project/build-context.js +2 -1
- package/dist/application/create-project/build-context.js.map +1 -1
- package/dist/application/create-project/index.d.ts +3 -1
- package/dist/application/create-project/index.d.ts.map +1 -1
- package/dist/application/create-project/index.js +4 -2
- package/dist/application/create-project/index.js.map +1 -1
- package/dist/application/install-skills/index.js +1 -1
- package/dist/application/install-skills/index.js.map +1 -1
- package/dist/core/create/execute-plan.d.ts +4 -0
- package/dist/core/create/execute-plan.d.ts.map +1 -1
- package/dist/core/create/execute-plan.js +8 -0
- package/dist/core/create/execute-plan.js.map +1 -1
- package/dist/core/create/types.d.ts +2 -0
- package/dist/core/create/types.d.ts.map +1 -1
- package/dist/domain/skills.d.ts +26 -0
- package/dist/domain/skills.d.ts.map +1 -1
- package/dist/domain/skills.js +13 -0
- package/dist/domain/skills.js.map +1 -1
- package/dist/index.js +53 -28
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/executor.d.ts.map +1 -1
- package/dist/infrastructure/create/executor.js +5 -0
- package/dist/infrastructure/create/executor.js.map +1 -1
- package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
- package/dist/infrastructure/create/lifecycle-registry.js +2 -0
- package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
- package/dist/infrastructure/create/setup-agent.d.ts +2 -0
- package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-agent.js +9 -8
- package/dist/infrastructure/create/setup-agent.js.map +1 -1
- package/dist/infrastructure/create/setup-dotenvx.d.ts +41 -0
- package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -0
- package/dist/infrastructure/create/setup-dotenvx.js +502 -0
- package/dist/infrastructure/create/setup-dotenvx.js.map +1 -0
- package/dist/infrastructure/create/setup-sanity.d.ts +13 -1
- package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-sanity.js +71 -31
- package/dist/infrastructure/create/setup-sanity.js.map +1 -1
- package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity/config.js +2 -1
- package/dist/modules/features/cms/sanity/config.js.map +1 -1
- package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity/mergers/layout.js +10 -0
- package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
- package/dist/modules/features/env/dotenvx/config.d.ts +13 -0
- package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -0
- package/dist/modules/features/env/dotenvx/config.js +47 -0
- package/dist/modules/features/env/dotenvx/config.js.map +1 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +8 -0
- package/dist/paths.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-skills/json-ld/SKILL.md +85 -0
- package/src/agent-skills/manage-env/SKILL.md +67 -0
- package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +48 -21
- package/src/modules/features/cms/sanity/files/components/sanity/structured-data.tsx +83 -0
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/queries.ts +14 -0
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +19 -3
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -0
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/index.ts +36 -1
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/metadata.ts +12 -1
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data-blocks.ts +416 -0
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data.ts +100 -0
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/structure.ts +37 -2
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +20 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +17 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +22 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +28 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +12 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +13 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +324 -15
- package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +45 -0
- package/src/modules/features/env/dotenvx/files/gitleaks.yml +25 -0
- package/src/modules/features/env/dotenvx/files/instrumentation.ts +14 -0
- package/src/modules/features/env/dotenvx/files/push-keys.mjs +129 -0
- package/src/modules/features/env/dotenvx/files/save-key.mjs +129 -0
- package/src/modules/features/env/dotenvx/files/setup-env.mjs +67 -0
- package/src/modules/features/env/dotenvx/files/vercelignore +4 -0
- package/src/modules/features/env/dotenvx/files/write-env.mjs +47 -0
- package/src/templates/next-default/.env.example +14 -6
- package/src/templates/next-default/.github/workflows/ci.yml +22 -1
- package/src/templates/next-default/.vscode/extensions.json +3 -0
- package/src/templates/next-default/README.md +31 -0
- package/src/templates/next-default/lib/utils/json-ld.tsx +327 -21
- package/src/templates/next-default/next.config.ts +6 -0
- package/src/templates/next-experiments/.env.example +14 -6
- package/src/templates/next-experiments/.github/workflows/ci.yml +22 -1
- package/src/templates/next-experiments/.vscode/extensions.json +3 -0
- package/src/templates/next-experiments/README.md +31 -0
- package/src/templates/next-experiments/lib/utils/json-ld.tsx +327 -21
- package/src/templates/next-experiments/next.config.ts +6 -0
- package/src/templates/next-pagebuilder/.env.example +19 -1
- package/src/templates/next-pagebuilder/.github/workflows/ci.yml +21 -1
- package/src/templates/next-pagebuilder/.vscode/extensions.json +3 -0
- package/src/templates/next-pagebuilder/README.md +31 -0
- package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +32 -19
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +38 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +36 -2
- package/src/templates/next-pagebuilder/next.config.ts +6 -0
- package/src/templates/next-webgl/.env.example +14 -6
- package/src/templates/next-webgl/.github/workflows/ci.yml +22 -1
- package/src/templates/next-webgl/.vscode/extensions.json +3 -0
- package/src/templates/next-webgl/README.md +31 -0
- package/src/templates/next-webgl/lib/utils/json-ld.tsx +327 -21
- package/src/templates/next-webgl/next.config.ts +6 -0
- package/src/modules/features/cms/sanity/files/lib/utils/json-ld.tsx +0 -244
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Save the private key to 1Password (Development vault) and point .env's op-ref
|
|
2
|
+
// at it, so teammates restore with `bun run env:setup`.
|
|
3
|
+
//
|
|
4
|
+
// bun run env:save
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
|
|
10
|
+
const VAULT = "Development";
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
console.error(message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Write the op-ref into .env (it's a plaintext line, never encrypted).
|
|
18
|
+
function setOpRef(ref) {
|
|
19
|
+
let content = existsSync(".env") ? readFileSync(".env", "utf-8") : "";
|
|
20
|
+
if (/^DOTENV_PRIVATE_KEY_OP_REF=.*$/m.test(content)) {
|
|
21
|
+
content = content.replace(
|
|
22
|
+
/^DOTENV_PRIVATE_KEY_OP_REF=.*$/m,
|
|
23
|
+
`DOTENV_PRIVATE_KEY_OP_REF=${ref}`,
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
27
|
+
content += `DOTENV_PRIVATE_KEY_OP_REF=${ref}\n`;
|
|
28
|
+
}
|
|
29
|
+
writeFileSync(".env", content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 1. Read the base private key from .env.keys.
|
|
33
|
+
if (!existsSync(".env.keys")) {
|
|
34
|
+
fail("No .env.keys found — set up dotenvx (or run `bun run env:setup`) first.");
|
|
35
|
+
}
|
|
36
|
+
const keyMatch = readFileSync(".env.keys", "utf-8").match(
|
|
37
|
+
/^\s*DOTENV_PRIVATE_KEY\s*=\s*(.+?)\s*$/m,
|
|
38
|
+
);
|
|
39
|
+
const privateKey = keyMatch?.[1]?.replace(/^["']|["']$/g, "");
|
|
40
|
+
if (!privateKey) fail("No DOTENV_PRIVATE_KEY found in .env.keys.");
|
|
41
|
+
|
|
42
|
+
// 2. Require the 1Password CLI; offer to sign in if the session is missing.
|
|
43
|
+
const whoami = spawnSync("op", ["whoami"], { stdio: "ignore" });
|
|
44
|
+
if (whoami.error?.code === "ENOENT") {
|
|
45
|
+
fail("1Password CLI not found — install `op` and run `op signin` first.");
|
|
46
|
+
}
|
|
47
|
+
if (whoami.status !== 0) {
|
|
48
|
+
// Sign-in is interactive, so it needs a TTY.
|
|
49
|
+
if (!process.stdin.isTTY) {
|
|
50
|
+
fail("Not signed in to 1Password — run `op signin` first.");
|
|
51
|
+
}
|
|
52
|
+
console.error("Not signed in to 1Password — launching `op signin`…");
|
|
53
|
+
spawnSync("op", ["signin"], { stdio: "inherit" });
|
|
54
|
+
// CLI-only sign-in doesn't persist the session, so confirm it took.
|
|
55
|
+
const recheck = spawnSync("op", ["whoami"], { stdio: "ignore" });
|
|
56
|
+
if (recheck.status !== 0) {
|
|
57
|
+
fail("Still not signed in to 1Password — run `op signin` and retry.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Prompt for the item title (default: package.json name) and an optional tag.
|
|
62
|
+
if (!process.stdin.isTTY) fail("env:save is interactive — run it in a terminal.");
|
|
63
|
+
let defaultTitle = "";
|
|
64
|
+
try {
|
|
65
|
+
defaultTitle = JSON.parse(readFileSync("package.json", "utf-8")).name ?? "";
|
|
66
|
+
} catch {
|
|
67
|
+
// no package.json name — leave the title prompt empty
|
|
68
|
+
}
|
|
69
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
const title =
|
|
71
|
+
(
|
|
72
|
+
await rl.question(
|
|
73
|
+
`1Password item title${defaultTitle ? ` [${defaultTitle}]` : ""}: `,
|
|
74
|
+
)
|
|
75
|
+
).trim() || defaultTitle;
|
|
76
|
+
const tag = (await rl.question("Tag (optional): ")).trim();
|
|
77
|
+
rl.close();
|
|
78
|
+
if (!title) fail("A title is required.");
|
|
79
|
+
|
|
80
|
+
// 4. Refuse if an item with that title already exists in the vault.
|
|
81
|
+
const existing = spawnSync("op", ["item", "get", title, "--vault", VAULT], {
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
});
|
|
84
|
+
if (existing.status === 0) {
|
|
85
|
+
fail(
|
|
86
|
+
`An item "${title}" already exists in ${VAULT} — rename or remove it first.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 5. Create the item. The key is piped via a JSON template (stdin) so it never
|
|
91
|
+
// lands in argv; category/title/tags are flags. `-` must be the first arg.
|
|
92
|
+
const args = [
|
|
93
|
+
"item",
|
|
94
|
+
"create",
|
|
95
|
+
"-",
|
|
96
|
+
"--category",
|
|
97
|
+
"Secure Note",
|
|
98
|
+
"--title",
|
|
99
|
+
title,
|
|
100
|
+
"--vault",
|
|
101
|
+
VAULT,
|
|
102
|
+
"--format",
|
|
103
|
+
"json",
|
|
104
|
+
];
|
|
105
|
+
if (tag) args.push("--tags", tag);
|
|
106
|
+
const template = {
|
|
107
|
+
fields: [{ label: "private-key", type: "CONCEALED", value: privateKey }],
|
|
108
|
+
};
|
|
109
|
+
const created = spawnSync("op", args, {
|
|
110
|
+
input: JSON.stringify(template),
|
|
111
|
+
encoding: "utf-8",
|
|
112
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
113
|
+
});
|
|
114
|
+
if (created.status !== 0) fail("\nFailed to create the 1Password item.");
|
|
115
|
+
|
|
116
|
+
// 6. Reference the item by id (stable; survives renames), falling back to title.
|
|
117
|
+
let itemRef = title;
|
|
118
|
+
try {
|
|
119
|
+
const id = JSON.parse(created.stdout)?.id;
|
|
120
|
+
if (id) itemRef = id;
|
|
121
|
+
} catch {
|
|
122
|
+
// non-JSON output — keep the title-based ref
|
|
123
|
+
}
|
|
124
|
+
const ref = `op://${VAULT}/${itemRef}/private-key`;
|
|
125
|
+
setOpRef(ref);
|
|
126
|
+
|
|
127
|
+
console.log(`\n✓ Saved to 1Password (${VAULT} / "${title}").`);
|
|
128
|
+
console.log(` Set DOTENV_PRIVATE_KEY_OP_REF=${ref} in .env.`);
|
|
129
|
+
console.log(" Teammates can now restore with `bun run env:setup`.");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Restore .env.keys (dotenvx private decryption keys) from 1Password via `op`.
|
|
2
|
+
// Store either the full .env.keys contents (recommended — covers every
|
|
3
|
+
// environment) or just the DOTENV_PRIVATE_KEY value in a 1Password item field;
|
|
4
|
+
// the reference lives in the committed .env (DOTENV_PRIVATE_KEY_OP_REF):
|
|
5
|
+
//
|
|
6
|
+
// DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup
|
|
7
|
+
//
|
|
8
|
+
// `--auto` (run from the preinstall hook) is for onboarding: it never fails
|
|
9
|
+
// `bun install` — it skips when .env.keys already exists, in CI, or when the
|
|
10
|
+
// reference / `op` CLI is missing.
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const AUTO = process.argv.includes("--auto");
|
|
16
|
+
|
|
17
|
+
// In --auto mode any unmet precondition exits 0 so the install never breaks.
|
|
18
|
+
const stop = (msg) => {
|
|
19
|
+
console.error(msg);
|
|
20
|
+
process.exit(AUTO ? 0 : 1);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Onboarding skips: nothing to do, or the wrong place to do it.
|
|
24
|
+
if (AUTO && existsSync(".env.keys")) process.exit(0);
|
|
25
|
+
if (AUTO && (process.env.CI || process.env.VERCEL)) process.exit(0);
|
|
26
|
+
|
|
27
|
+
const OP_REF = process.env.DOTENV_PRIVATE_KEY_OP_REF;
|
|
28
|
+
if (!OP_REF) {
|
|
29
|
+
stop(
|
|
30
|
+
"✗ set DOTENV_PRIVATE_KEY_OP_REF to the 1Password reference for .env.keys\n" +
|
|
31
|
+
' e.g. DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const res = spawnSync("op", ["read", OP_REF], { encoding: "utf8" });
|
|
36
|
+
|
|
37
|
+
if (res.error) {
|
|
38
|
+
const why =
|
|
39
|
+
res.error.code === "ENOENT"
|
|
40
|
+
? "install the 1Password CLI (`op`) and sign in — https://developer.1password.com/docs/cli/get-started/"
|
|
41
|
+
: res.error.message;
|
|
42
|
+
stop(`✗ failed to run \`op\`: ${why}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (res.status !== 0) {
|
|
46
|
+
stop(`✗ \`op read\` failed:\n${res.stderr.trim()}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const HEADER = `#/------------------!DOTENV_PRIVATE_KEYS!-------------------/
|
|
50
|
+
#/ private decryption keys. DO NOT commit to source control /
|
|
51
|
+
#/ [how it works](https://dotenvx.com/encryption) /
|
|
52
|
+
#/ ⛨ ARMORED KEYS: \`dotenvx armor up\` /
|
|
53
|
+
#/----------------------------------------------------------/
|
|
54
|
+
|
|
55
|
+
# .env
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const value = res.stdout.trim();
|
|
59
|
+
|
|
60
|
+
// op may hold the full .env.keys file or just the bare private key. Pass full
|
|
61
|
+
// contents through; wrap a bare key in the standard .env.keys format.
|
|
62
|
+
const contents = value.includes("DOTENV_PRIVATE_KEY")
|
|
63
|
+
? `${value}\n`
|
|
64
|
+
: `${HEADER}DOTENV_PRIVATE_KEY=${value}\n`;
|
|
65
|
+
|
|
66
|
+
writeFileSync(".env.keys", contents);
|
|
67
|
+
console.info("✓ wrote .env.keys from 1Password");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Add or (re)encrypt env values without remembering the dotenvx flags.
|
|
2
|
+
//
|
|
3
|
+
// bun run env:set KEY value # encrypt one value into .env
|
|
4
|
+
// bun run env:set KEY value --plain # store it plaintext (e.g. NEXT_PUBLIC_*)
|
|
5
|
+
// bun run env:set KEY value --prod # target .env.production (--preview too)
|
|
6
|
+
// bun run env:encrypt-all # bulk-encrypt every plaintext value in .env
|
|
7
|
+
// bun run env:encrypt-all --prod # bulk-encrypt .env.production
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
const [mode, ...rest] = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
// --prod / --preview pick the per-environment file; default is the shared .env.
|
|
14
|
+
const ENV_FLAGS = new Set(["--prod", "--production", "--preview"]);
|
|
15
|
+
const file =
|
|
16
|
+
rest.includes("--prod") || rest.includes("--production")
|
|
17
|
+
? ".env.production"
|
|
18
|
+
: rest.includes("--preview")
|
|
19
|
+
? ".env.preview"
|
|
20
|
+
: ".env";
|
|
21
|
+
|
|
22
|
+
// On bulk encrypt, keep browser-public NEXT_PUBLIC_* and the 1Password pointer
|
|
23
|
+
// plaintext (same exclusions the scaffold uses).
|
|
24
|
+
const EXCLUDE = ["--exclude-key", "DOTENV_PRIVATE_KEY_OP_REF", "NEXT_PUBLIC_*"];
|
|
25
|
+
|
|
26
|
+
function run(args) {
|
|
27
|
+
// `dotenvx` resolves from node_modules/.bin when run via `bun run`.
|
|
28
|
+
const res = spawnSync("dotenvx", args, { stdio: "inherit" });
|
|
29
|
+
process.exit(res.status ?? 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (mode === "encrypt") {
|
|
33
|
+
run(["encrypt", "-f", file, ...EXCLUDE]);
|
|
34
|
+
} else if (mode === "set") {
|
|
35
|
+
// Forward KEY, value and any dotenvx flags (e.g. --plain) straight through.
|
|
36
|
+
const passthrough = rest.filter((arg) => !ENV_FLAGS.has(arg));
|
|
37
|
+
if (passthrough.filter((arg) => !arg.startsWith("-")).length < 2) {
|
|
38
|
+
console.error(
|
|
39
|
+
"Usage: bun run env:set KEY value [--plain] [--preview|--prod]",
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
run(["set", ...passthrough, "-f", file]);
|
|
44
|
+
} else {
|
|
45
|
+
console.error(`Unknown env command: ${mode ?? "(none)"} (use set | encrypt)`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# ============================================
|
|
2
2
|
# Basement Next Starter - Environment Variables
|
|
3
3
|
# ============================================
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# This project uses dotenvx (https://dotenvx.com): the committed `.env` holds
|
|
5
|
+
# shared values, with secrets encrypted there. The private keys live in
|
|
6
|
+
# `.env.keys` (gitignored) — restore them with `bun run env:setup`.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
# Add or change a variable in `.env` (not .env.local):
|
|
9
|
+
# - Secret (API key, token, …): bun run env:set KEY value (--preview / --prod)
|
|
10
|
+
# - Trivial / NEXT_PUBLIC_*: bun run env:set KEY value --plain
|
|
11
|
+
# - Bulk-encrypt a file edited by hand: bun run env:encrypt-all (--preview / --prod)
|
|
12
|
+
#
|
|
13
|
+
# Edge runtime (middleware, `runtime = "edge"`): dotenvx can't decrypt there.
|
|
14
|
+
# Set any secret those read directly in Vercel's env vars, not here.
|
|
15
|
+
#
|
|
16
|
+
# This file just documents the available variables; real values live in `.env`.
|
|
9
17
|
# ============================================
|
|
10
18
|
|
|
11
19
|
|
|
@@ -16,7 +24,7 @@
|
|
|
16
24
|
# Base URL for your site (used for SEO, canonical URLs, etc.)
|
|
17
25
|
# Development: http://localhost:3000
|
|
18
26
|
# Production: https://your-domain.com
|
|
19
|
-
NEXT_PUBLIC_BASE_URL="
|
|
27
|
+
NEXT_PUBLIC_BASE_URL=""
|
|
20
28
|
|
|
21
29
|
# Draft mode secret for preview functionality
|
|
22
30
|
# Generate a random string: openssl rand -base64 32
|
|
@@ -32,7 +40,7 @@ DRAFT_MODE_TOKEN=""
|
|
|
32
40
|
|
|
33
41
|
# Required for Sanity
|
|
34
42
|
NEXT_PUBLIC_SANITY_PROJECT_ID=""
|
|
35
|
-
NEXT_PUBLIC_SANITY_DATASET="
|
|
43
|
+
NEXT_PUBLIC_SANITY_DATASET=""
|
|
36
44
|
NEXT_PUBLIC_SANITY_API_READ_TOKEN=""
|
|
37
45
|
|
|
38
46
|
# Optional - for server-side mutations and visual editing
|
|
@@ -46,8 +46,29 @@ jobs:
|
|
|
46
46
|
- name: Lint
|
|
47
47
|
run: bun run lint
|
|
48
48
|
|
|
49
|
+
# dotenvx needs the private key to decrypt secrets at build time. It's
|
|
50
|
+
# pulled from 1Password via an org-wide service-account token + the op-ref
|
|
51
|
+
# committed in .env. Optional: builds pass without it (only plaintext /
|
|
52
|
+
# NEXT_PUBLIC_* is read), and a per-repo DOTENV_PRIVATE_KEY secret still
|
|
53
|
+
# works as a fallback.
|
|
54
|
+
- name: Install 1Password CLI
|
|
55
|
+
if: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN != '' }}
|
|
56
|
+
uses: 1password/install-cli-action@v1
|
|
57
|
+
|
|
49
58
|
- name: Build
|
|
50
|
-
|
|
59
|
+
env:
|
|
60
|
+
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
|
61
|
+
DOTENV_PRIVATE_KEY: ${{ secrets.DOTENV_PRIVATE_KEY }}
|
|
62
|
+
run: |
|
|
63
|
+
if [ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
|
|
64
|
+
ref="$(grep -m1 '^DOTENV_PRIVATE_KEY_OP_REF=' .env | cut -d= -f2-)"
|
|
65
|
+
if [ -n "$ref" ]; then
|
|
66
|
+
DOTENV_PRIVATE_KEY="$(op read "$ref")"
|
|
67
|
+
echo "::add-mask::$DOTENV_PRIVATE_KEY"
|
|
68
|
+
export DOTENV_PRIVATE_KEY
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
bun run build
|
|
51
72
|
|
|
52
73
|
# Runs your tests if any exist; a fresh project with none passes cleanly.
|
|
53
74
|
- name: Test
|
|
@@ -22,6 +22,35 @@ bun dev
|
|
|
22
22
|
| `bun format` | Format the codebase |
|
|
23
23
|
| `bun typecheck` | Run TypeScript |
|
|
24
24
|
| `bun analyze` | Analyze the Next.js bundle |
|
|
25
|
+
| `bun env:set KEY value` | Encrypt one value into `.env` (`--plain`, `--preview`/`--prod`) |
|
|
26
|
+
| `bun env:encrypt-all` | Bulk-encrypt every plaintext value in a file (`--preview`/`--prod`) |
|
|
27
|
+
| `bun env:setup` | Restore `.env.keys` from 1Password |
|
|
28
|
+
| `bun env:view` | Print the decrypted `.env` (`--preview` / `--prod`) |
|
|
29
|
+
| `bun env:setup-remote` | Push private keys to GitHub / Vercel (`github`/`vercel`, or pick) |
|
|
30
|
+
| `bun env:save` | Save the private key to 1Password (Development vault) |
|
|
31
|
+
|
|
32
|
+
## Environment variables
|
|
33
|
+
|
|
34
|
+
Secrets are encrypted at rest with [dotenvx](https://dotenvx.com): the committed `.env` holds encrypted values, the private keys live in `.env.keys` (gitignored), and dev/build run through `dotenvx run` so values decrypt automatically.
|
|
35
|
+
|
|
36
|
+
**First run.** `bun install` restores `.env.keys` from 1Password (via `DOTENV_PRIVATE_KEY_OP_REF` in `.env`), or run `bun run env:setup`. Needs 1Password access + the `op` CLI:
|
|
37
|
+
|
|
38
|
+
- macOS: `brew install --cask 1password-cli`
|
|
39
|
+
- Windows: `winget install 1password-cli`
|
|
40
|
+
- Linux / other: [1Password CLI docs](https://developer.1password.com/docs/cli/get-started/)
|
|
41
|
+
|
|
42
|
+
**Add or change a variable** (in `.env`):
|
|
43
|
+
|
|
44
|
+
- Secret: `bun run env:set KEY value` (`--preview`/`--prod` to target an environment)
|
|
45
|
+
- Public / `NEXT_PUBLIC_*`: `bun run env:set KEY value --plain`
|
|
46
|
+
- Bulk-encrypt a hand-edited file: `bun run env:encrypt-all`
|
|
47
|
+
- Inspect: `bun run env:view` (`--preview`/`--prod`)
|
|
48
|
+
|
|
49
|
+
**Per environment.** Scripts load `.env.$VERCEL_ENV` over `.env`. After adding a per-env file, push its key with `bun run env:setup-remote vercel` — `instrumentation.ts` uses it to decrypt at runtime.
|
|
50
|
+
|
|
51
|
+
**Edge runtime.** dotenvx decrypts only in the Node runtime; secrets read in `middleware.ts` or a `runtime = "edge"` route must be set as plain Vercel env vars.
|
|
52
|
+
|
|
53
|
+
A gitleaks pre-commit hook + CI scan block committed plaintext secrets (install: `brew install gitleaks`).
|
|
25
54
|
|
|
26
55
|
## Project Structure
|
|
27
56
|
|
|
@@ -60,6 +89,8 @@ The workflow takes its runtime versions from your manifests, so there's a single
|
|
|
60
89
|
|
|
61
90
|
It also caches Bun's install cache between runs, and the test step is a no-op until you add real test files.
|
|
62
91
|
|
|
92
|
+
The build decrypts the committed `.env` with the dotenvx private key, pulled from 1Password: set an **org-level** `OP_SERVICE_ACCOUNT_TOKEN` (a read-only [service account](https://developer.1password.com/docs/service-accounts/) scoped to the `Development` vault) — one secret for every repo — and CI reads the key via the op-ref committed in `.env`. A per-repo `DOTENV_PRIVATE_KEY` secret (`bun run env:setup-remote github`) works as a fallback; builds pass without either (only plaintext / `NEXT_PUBLIC_*` is read).
|
|
93
|
+
|
|
63
94
|
### Branch protection
|
|
64
95
|
|
|
65
96
|
To block merges when CI fails, enable branch protection on `main`:
|