Skip to content

feat: --provision-keys for owner key on pod creation (#437 Phase 1)#442

Merged
melvincarvalho merged 6 commits into
gh-pagesfrom
issue-437-provision-keys-phase-1
May 14, 2026
Merged

feat: --provision-keys for owner key on pod creation (#437 Phase 1)#442
melvincarvalho merged 6 commits into
gh-pagesfrom
issue-437-provision-keys-phase-1

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

Adds a --provision-keys CLI flag (and provisionKeys: true body field on POST /.pods) that, on pod creation, generates a Schnorr secp256k1 keypair and writes it as a W3C CID v1.0 Multikey document at <pod>/private/privkey.jsonld.

Off by default — keys-on-disk is a security tradeoff and we want operators to opt in deliberately.

The single key is intended to serve as a Solid signing identity, a Nostr identity (same curve), and a did:nostr: DID controller in Phase 2. One CLI flag → pod-resident self-sovereign identity ready for both Solid and Nostr / agentic use cases.

Phase 1 of #437. Closes that issue's Phase 1 acceptance criteria. The same issue body has the design resolutions for the six pre-implementation questions (path choice, controller in Phase 1, no nostr block, opt-in, multi-user via HTTP body, plaintext-with-mitigations).

Usage

# Single-user (CLI flag)
jss start --single-user --provision-keys

# Multi-user (HTTP body field)
curl -X POST http://localhost:4444/.pods \
  -H "Content-Type: application/json" \
  -d '{ "name": "alice", "provisionKeys": true }'

# Env var (same effect, useful in containers)
JSS_PROVISION_KEYS=true jss start --single-user

What gets written

{
  "@context": "https://www.w3.org/ns/cid/v1",
  "type": "Multikey",
  "controller": "https://your.example/profile/card.jsonld#me",
  "publicKeyMultibase": "fe70102…",
  "secretKeyMultibase": "f81260a1…"
}

Encoding: f-form Multikey (multibase f + multicodec varint + payload, base16-lower) — matches src/auth/nostr-keys.js's existing decoder. Round-trips through decodeFFormSecp256k1. No new dependencies.

File mode: 0o600 on POSIX. WAC protects HTTP access (owner-only on /private/); the file mode blocks a separate exposure class (other unix users on the host, backups that don't preserve permissions). Defence-in-depth, not a replacement for FDE / OS keyring.

Design decisions (logged in the issue body)

  1. Plaintext on disk in Phase 1. Mitigations: WAC owner-only, file mode 0o600, prominent BACK UP banner, docs nudge toward FDE / LUKS / OS keyring. Wrapped secrets land in Phase 3.
  2. No nostr extension block. Bech32 npub is a one-line derivation in any Nostr-aware tool; including it would double the secret-on-disk surface for log/error/dump leaks. The secretKeyMultibase IS the secret, in only one place.
  3. Controller = pod owner WebID in Phase 1. Phase 2 will swap in did:nostr: once the resolver lands. The document is self-consistent at every phase — strict CID consumers won't see unresolvable references.
  4. Path /private/privkey.jsonld. Deviation from SolidOS's /settings/keys/ convention. /private/ is the obvious "private namespace" and gets owner-only WAC by default in JSS-created pods. Documented to prevent future "fix" PRs.
  5. Multi-user via HTTP body field. No new jss create-pod subcommand; the flag rides on the existing POST /.pods surface so the two paths stay in lockstep.
  6. Opt-in. Defaulting on would silently ship a plaintext secret on every jss start --single-user. Visible choice required.

Tests

840/840 pass (22 new):

  • test/keys-provision.test.js — 13 unit tests:
    • Keypair generation: returns valid Schnorr keypair (sign/verify round-trip), produces unique keypairs across calls
    • Encoder fixed-vector tests for publicKeyMultibase / secretKeyMultibase
    • Round-trip through the existing decodeFFormSecp256k1 decoder in src/auth/nostr-keys.js
    • buildOwnerKeyDocument shape (W3C CID v1.0 conformant; WebID controller in Phase 1; no nostr block)
    • Error cases: non-hex / wrong-length inputs, missing controller
  • test/keys-provision-integration.test.js — 9 integration tests:
    • POST /.pods with provisionKeys: true writes the file, response surfaces the public side, response never echoes the secret
    • File mode is 0o600 on POSIX (skipped on Windows)
    • WAC blocks unauthenticated reads (401), serves authenticated owner (200)
    • Opt-in respected: no flag, provisionKeys: false, and provisionKeys: "true" (string, not boolean) all result in no file written
    • createPodStructure direct call returns ownerKey only when option is set

Test plan

  • npm test — 840/840 passing locally
  • Unit tests pin the encoding format (fixed vectors)
  • Integration tests cover the wired flow (HTTP + direct)
  • File mode + WAC behaviour both covered

Out of scope (deferred to later phases per #437)

  • did:nostr controller (Phase 2)
  • WebID profile auto-population with the public key (Phase 2)
  • --key-passphrase / scrypt+aesgcm wrap (Phase 3)
  • BIP-39 seed phrase derivation (Phase 3)
  • Hardware wallets / KMS / encrypted-at-rest (Phase 4)

Closes #437 Phase 1.

Add a `--provision-keys` flag (and `provisionKeys: true` body field for
POST /.pods) that, on pod creation, generates a Schnorr secp256k1
keypair and writes it as a W3C CID v1.0 Multikey document to
<pod>/private/privkey.jsonld. Off by default — keys-on-disk is a
security tradeoff and we want operators to opt in deliberately.

The single key is intended to serve as: a Solid signing identity, a
Nostr identity (same curve), and a did:nostr DID controller (Phase 2).
One CLI flag → pod-resident self-sovereign identity ready for both
Solid and Nostr / agentic use cases.

Phase 1 scope (per #437 design resolutions):

- Encoding: f-form Multikey (multibase `f` + multicodec varint +
  payload, base16-lower) — matches src/auth/nostr-keys.js's existing
  decoder, no new deps. Round-trips through decodeFFormSecp256k1.
- Controller: pod owner WebID. Phase 2 will swap in did:nostr once
  the resolver lands; the document is self-consistent at every phase.
- File mode: 0o600 on POSIX. WAC restricts HTTP access; the file mode
  blocks a separate exposure class (other unix users on the box,
  backups that don't preserve permissions). Defence-in-depth, not a
  replacement for FDE / OS keyring.
- No `nostr` extension block (no nsec/npub). Bech32 npub is a
  one-line derivation in any Nostr-aware tool; including it would
  double the secret-on-disk surface for log/error/dump leaks.
- Opt-in: defaulting on would silently ship a plaintext secret on
  every `jss start --single-user`. Visible choice required.
- HTTP response surfaces `ownerKey: { keyDocument, publicKeyMultibase }`
  on success — never the secret. Single-user banner logs the same and
  a prominent BACK UP warning.

Files:

- src/keys/provision.js — generator + Multikey serializer + the
  decideRevealForRegisterStatus-style pure helpers for unit testing.
- src/handlers/container.js — createPodStructure accepts
  { provisionKeys: true }; handleCreatePod reads provisionKeys body
  field; response includes the key info when set.
- src/server.js — createRootPodStructure accepts the option via the
  new provisionKeysEnabled local; single-user onReady captures the
  key result and logs the public side + backup warning.
- src/storage/filesystem.js — write() takes an optional { mode } so
  callers can request 0o600 without a separate chmod call. Backward
  compatible (every existing caller passes nothing, mode no-ops).
- src/config.js — defaults.provisionKeys, BOOLEAN_KEYS membership,
  JSS_PROVISION_KEYS env var mapping.
- bin/jss.js — --provision-keys / --no-provision-keys CLI options,
  threaded through to createServer.
- docs/provision-keys.md — usage, file format, threat model with the
  filesystem-protection table, credible-exit framing for the upgrade
  path, backup instructions.
- test/keys-provision.test.js — 13 unit tests: keypair generation
  (sign/verify roundtrip, uniqueness), encoder fixed vectors,
  decoder round-trip via src/auth/nostr-keys, JSON-LD shape, no
  nostr block, error cases.
- test/keys-provision-integration.test.js — 9 integration tests:
  POST /.pods writes the file + Multikey shape, file mode is 0o600
  on POSIX, WAC blocks unauthenticated reads + serves authenticated
  owner, opt-in respected (no flag, false, string "true"), direct
  createPodStructure call returns ownerKey only when option set.

840/840 tests pass.

Out of scope (deferred to later phases per #437):
- did:nostr controller (Phase 2)
- WebID profile auto-population with the public key (Phase 2)
- --key-passphrase / scrypt+aesgcm wrap (Phase 3)
- BIP-39 seed phrase derivation (Phase 3)
- Hardware wallets / KMS / encrypted-at-rest (Phase 4)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in owner key provisioning during pod creation (Phase 1 of #437), generating a Schnorr secp256k1 keypair and persisting it as a W3C CID v1.0 Multikey JSON-LD document at <pod>/private/privkey.jsonld, with CLI/config/HTTP wiring and test coverage.

Changes:

  • Introduces --provision-keys / JSS_PROVISION_KEYS (single-user startup) and provisionKeys: true on POST /.pods (multi-user) to generate + write the Multikey document.
  • Extends filesystem storage writes to optionally tighten file mode (intended for 0o600 secrets) and surfaces only public key info in startup logs / pod-create HTTP responses.
  • Adds unit + integration tests and a new operator-facing doc page for threat model/backup guidance.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/keys-provision.test.js Unit tests for key generation, multibase encoding, decoder round-trip, and document shape.
test/keys-provision-integration.test.js Integration coverage for POST /.pods provisioning, WAC access, and POSIX file mode expectations.
src/storage/filesystem.js Adds optional { mode } parameter to write() to support secret-file permission tightening.
src/server.js Wires provisionKeys into single-user pod creation and logs public-only provisioning info + backup warning.
src/keys/provision.js New key provisioning module: generate keypair, encode Multikey fields, build JSON-LD document.
src/handlers/container.js Wires provisionKeys body flag into pod creation and returns a public-only summary in API responses.
src/config.js Adds provisionKeys default + env var mapping + boolean coercion support.
docs/provision-keys.md New documentation for usage, format, threat model, and backup guidance.
bin/jss.js Adds CLI flags and passes provisionKeys through into server options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/storage/filesystem.js
Comment on lines 86 to +101
// Ensure parent directory exists
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content);
// Optional file-mode tightening (e.g. 0o600 for secret material).
// No-op on Windows, where chmod permissions are coarse — callers
// should not rely on POSIX modes for cross-platform security.
if (typeof options.mode === 'number') {
try {
await fs.chmod(filePath, options.mode);
} catch (chmodErr) {
// Don't fail the write because the chmod didn't take — log and
// continue. Callers that care about strict permissions (secret
// material) should additionally rely on filesystem-level
// protection (FDE, OS keyring, container user namespacing).
console.warn(`chmod ${options.mode.toString(8)} on ${filePath} failed:`, chmodErr.message);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: storage.write now passes mode to fs.writeFile at create time so the file is born with the requested permissions, closing the race window where another local process could read freshly created secret material. The follow-up chmod is kept as belt-and-braces for the overwrite case (Node only honours mode at create time, never on overwrite).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming this is already addressed (since commit 2de873e). The actual fix is at lines 108-113 in the current file: when options.mode is set, fs.writeFile is called with { mode: options.mode } so the file is created with the requested permissions — the race window between create-with-default-umask and follow-up chmod is closed. The chmod block this comment is anchored to is the belt-and-braces step that handles the overwrite case (where Node only applies mode at create time, not on overwrite). Reading the file top-to-bottom: writeFile-with-mode at the top, chmod-as-fallback at the bottom — both inside the same typeof options.mode === 'number' guard.

Comment thread src/server.js Outdated
Comment on lines +1012 to +1013
* surface the public side in the startup banner. The secret is never
* returned (it's only on disk under /private/, mode 0600).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: tightened the docstring to match reality — the returned ownerKey does include secretHex/secretKeyMultibase (needed for tests and future one-shot signing before WAC reads the file back). Comment now says 'callers must not log the secret' explicitly, with a note that the only durable home for the secret is the on-disk file under owner-only WAC + 0o600.

Comment thread src/server.js Outdated
Comment on lines +866 to +868
const keyPath = isRootPod
? `${podUri}private/privkey.jsonld`
: `${podUri}private/privkey.jsonld`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: collapsed the redundant ternary to a single expression. podUri already carries the trailing slash + name segment, so the same string covers root and named single-user pods.

Comment thread docs/provision-keys.md Outdated
@@ -0,0 +1,137 @@
# Owner-key provisioning (`--provision-keys`)

Phase 1 of [#427 / #437](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/437). Generates a Schnorr secp256k1 keypair on pod creation and writes it as a W3C [Controlled Identifiers v1.0 Multikey](https://www.w3.org/TR/cid-1.0/) document at `<pod>/private/privkey.jsonld`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: dropped the #427 reference. Just #437.

Comment thread docs/provision-keys.md Outdated
Comment on lines +122 to +123
# Or copy from disk if you have local access (preserves perms)
cp -p /path/to/data/private/privkey.jsonld pod-key-backup.jsonld

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: added named-pod path variants for both the HTTP curl backup and the on-disk cp backup. The multi-user case (and single-user with --single-user-name=alice) puts the file under <DATA_ROOT>/alice/private/privkey.jsonld, distinct from the root-pod layout.

Comment thread src/storage/filesystem.js
Comment on lines 76 to 83
/**
* Write resource content
* @param {string} urlPath
* @param {Buffer | string} content
* @returns {Promise<boolean>}
*/
export async function write(urlPath, content) {
export async function write(urlPath, content, options = {}) {
const filePath = urlToPath(urlPath);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2de873e: full JSDoc for write() now documents the options.mode arg with rationale (race-window closure, Windows no-op, secret-material use case).

Six small but real fixes from the first review pass.

1. storage.write created the file with the process umask (often 0644)
   and only then chmod'd to the requested mode. Race window where
   another local process could read freshly created secret material
   before chmod ran. Pass `mode` to fs.writeFile at create time so
   the file is *born* with the right perms; keep the chmod afterward
   as belt-and-braces for the overwrite case (Node only honours mode
   at create time, not on overwrite).
2. createRootPodStructure's docstring said the secret was "never
   returned", but the returned ownerKey object includes secretHex
   and secretKeyMultibase. Tightened the doc to match reality —
   "callers must not log the secret" — and noted why we keep the
   internal representation full (tests, future one-shot signing).
3. Single-user onReady built the same `keyPath` string in both
   branches of an isRootPod ternary. Collapsed to one expression
   (podUri already carries the trailing slash + name segment).
4. docs/provision-keys.md intro referenced "#427 / #437" — but #427
   is the unrelated ACL-portability umbrella. Just #437.
5. docs backup example only showed the root-pod path. Added the
   named-pod variants for both the HTTP curl backup and the on-disk
   cp backup, since the named-pod layout is the multi-user case.
6. Updated storage.write JSDoc to document the new `options.mode`
   argument with the rationale.

840/840 tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Comment thread src/handlers/container.js
Comment on lines +358 to +366
// Surface a summary of any provisioned key so callers can display
// the public side. The secret is NEVER echoed in the response —
// it lives only on disk under the pod's owner-only ACL.
const keyInfo = podCreation?.ownerKey
? {
keyDocument: `${podUri}private/privkey.jsonld`,
publicKeyMultibase: podCreation.ownerKey.publicMultibase
}
: null;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14c543d: refused the provisionKeys + --public combination at every entry point. New assertProvisionKeysCompatible() exported from src/keys/provision.js; createServer throws synchronously if both flags are set (server doesn't start), handleCreatePod returns 400 if a POST /.pods sends provisionKeys: true on a --public server, and docs/provision-keys.md gains an Incompatible-with-public section spelling out both refusal points. Tests cover the full truth table for the assertion plus an HTTP-level 400 integration test.

Comment thread src/server.js
Comment on lines +861 to +877
// Surface the public side of any provisioned owner key, plus
// a prominent backup reminder. The secret is NOT logged — it
// lives on disk only, under /private/privkey.jsonld with
// owner-only WAC and file mode 0o600.
if (creation?.ownerKey) {
// `podUri` already includes the trailing slash + any pod
// name segment, so the same expression covers root and
// named single-user pods.
const keyPath = `${podUri}private/privkey.jsonld`;
fastify.log.info(`Provisioned Schnorr secp256k1 owner key`);
fastify.log.info(` Public key file: ${keyPath}`);
fastify.log.info(` publicKeyMultibase: ${creation.ownerKey.publicMultibase}`);
fastify.log.warn(
`BACK UP ${keyPath} — losing this file means losing this identity. ` +
'Filesystem reads bypass WAC; use FDE / OS keyring / restrictive umask ' +
'for any pod that matters. See docs/provision-keys.md.'
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14c543d: refused the provisionKeys + --public combination at every entry point. New assertProvisionKeysCompatible() exported from src/keys/provision.js; createServer throws synchronously if both flags are set (server doesn't start), handleCreatePod returns 400 if a POST /.pods sends provisionKeys: true on a --public server, and docs/provision-keys.md gains an Incompatible-with-public section spelling out both refusal points. Tests cover the full truth table for the assertion plus an HTTP-level 400 integration test.

Comment thread docs/provision-keys.md
Comment on lines +69 to +80
The file mode is set to `0o600` on POSIX. WAC restricts HTTP access to the pod owner via `<pod>/private/.acl` (the standard private-folder ACL JSS writes anyway).

## Threat model and protection

The web user (pod owner) reads the key over HTTP, authenticated against their WebID. WAC checks the request and serves the file:

```bash
curl -H "Authorization: Bearer <owner-token>" \
http://localhost:4444/alice/private/privkey.jsonld
```

That path is fully protected by ACL. **What WAC cannot protect against is filesystem access.** Anyone with read on the data directory bypasses WAC entirely:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14c543d: refused the provisionKeys + --public combination at every entry point. New assertProvisionKeysCompatible() exported from src/keys/provision.js; createServer throws synchronously if both flags are set (server doesn't start), handleCreatePod returns 400 if a POST /.pods sends provisionKeys: true on a --public server, and docs/provision-keys.md gains an Incompatible-with-public section spelling out both refusal points. Tests cover the full truth table for the assertion plus an HTTP-level 400 integration test.

Address Copilot pickup on #442. --public bypasses WAC and grants
unauthenticated access to every resource — including any
/private/privkey.jsonld we'd just written. The two flags are
explicitly incompatible; refuse the combination at every entry point
so the operator hits the contradiction immediately rather than by
reading a leaked key from logs or the public web.

- New assertProvisionKeysCompatible({ provisionKeys, isPublic })
  exported from src/keys/provision.js. Throws with a clear error
  message naming both flags and the WAC bypass.
- src/server.js calls the assertion at server-create time. A
  `jss start --single-user --provision-keys --public` invocation
  now fails to start instead of silently writing a public secret.
- src/handlers/container.js handleCreatePod returns HTTP 400 when
  request.config?.public is true and provisionKeys: true is sent.
  Pod is not created; error explains why.
- docs/provision-keys.md gains an "Incompatible with --public"
  section spelling out both refusal points.
- 4 new unit tests for the assertion (full truth table).
- 1 new HTTP integration test exercising the 400 path on a
  --public test server.
- 1 new createServer-throw test asserting the synchronous refusal
  on construction.

847/847 tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Comment thread src/handlers/container.js
import { isContainer, getEffectiveUrlPath, getPodName } from '../utils/url.js';
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl, relativizeOwnerWebId } from '../wac/parser.js';
import { provisionOwnerKey, assertProvisionKeysCompatible } from '../keys/provision.js';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eaa25d2: switched the call site to use assertProvisionKeysCompatible (the dead-import warning is gone) and converted the throw into a 400 response — single source of truth for the error message shared with createServer's startup-time check.

Comment thread src/handlers/container.js Outdated
Comment on lines +256 to +260
await storage.write(
`${podPath}private/privkey.jsonld`,
JSON.stringify(ownerKey.document, null, 2),
{ mode: 0o600 }
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eaa25d2: storage.write's boolean return is now checked at both call sites. createPodStructure throws on !ok so the existing handleCreatePod cleanup path (storage.remove + 500) runs. createRootPodStructure also throws so single-user startup fails loud rather than logging 'Provisioned …' against a missing file.

Comment thread src/server.js Outdated
Comment on lines +1091 to +1095
await storage.write(
'/private/privkey.jsonld',
JSON.stringify(ownerKey.document, null, 2),
{ mode: 0o600 }
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eaa25d2: storage.write's boolean return is now checked at both call sites. createPodStructure throws on !ok so the existing handleCreatePod cleanup path (storage.remove + 500) runs. createRootPodStructure also throws so single-user startup fails loud rather than logging 'Provisioned …' against a missing file.

Three Copilot pickups on #442 (round 3).

1. handleCreatePod imported assertProvisionKeysCompatible last round
   but was still doing the public-mode check inline — dead import.
   Switch the call site to the helper and convert the throw into a
   400 response. Single source of truth for the error message,
   shared with createServer's startup-time check.

2. createPodStructure called storage.write() for the secret key but
   ignored the boolean return. storage.write returns false (without
   throwing) on failure, so a write that silently failed would have
   left the pod in a state where the response advertised an
   `ownerKey` against an on-disk file that didn't exist. Throw on
   `!ok` so the existing handleCreatePod cleanup path (storage.remove
   + 500) runs.

3. createRootPodStructure had the same gap: single-user startup
   could log "Provisioned Schnorr secp256k1 owner key" against a
   missing file. Throw on `!ok` so the start fails loud — the
   operator explicitly asked for --provision-keys, a silent failure
   would surface only when they later tried to read the key.

847/847 tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Comment thread src/handlers/container.js Outdated
// pod isn't left with a phantom `ownerKey` in the response that
// doesn't correspond to any on-disk file.
let ownerKey;
if (options.provisionKeys) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ac78dad: switched createPodStructure to strict === true on options.provisionKeys, matching handleCreatePod's HTTP-side check on the body field. New regression test asserts the direct-entry strict check (string 'true' doesn't trigger). 848/848 tests pass.

Comment thread src/storage/filesystem.js Outdated
Comment on lines +87 to +90
* looser default; an additional `chmod` runs afterward to tighten
* the file when overwriting an existing path that was created with
* a wider mode. No-op on Windows. See #437 for the secret-material
* use case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ac78dad: updated the JSDoc and inline comment to reflect actual behaviour — chmod is attempted on every platform (NTFS approximates POSIX modes coarsely as a read-only flag based on owner perms), callers should not rely on it for cross-platform secrecy. No platform guard added; the warn-on-failure path catches anything weird.

Two more Copilot pickups on #442.

1. createPodStructure activated key provisioning on any truthy
   options.provisionKeys — inconsistent with handleCreatePod's
   strict === true check on the request body. A misconfigured
   direct caller passing 'true' / 1 / etc. would have silently
   provisioned a plaintext secret. Match the HTTP-side behaviour:
   strict === true at both entry points. New regression test asserts
   string 'true' doesn't trigger at the direct entry.

2. storage.write JSDoc said the chmod was a 'No-op on Windows', but
   the implementation always attempts chmod regardless of platform.
   Update the doc to reflect the actual behaviour: chmod is best-
   effort everywhere, NTFS approximates POSIX modes coarsely (read-
   only flag based on owner perms), callers should not rely on it
   for cross-platform secrecy. Inline comment also clarified.

848/848 tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Comment thread src/server.js
Comment on lines +141 to +153
// Provision a Schnorr secp256k1 owner key in /private/privkey.jsonld
// when a single-user pod is first created. Phase 1 of #437. Off by
// default: keys-on-disk is a real security tradeoff, opt-in keeps
// the choice visible to the operator.
//
// Refuse the --provision-keys + --public combination at server-create
// time so the operator hits the contradiction immediately rather than
// by reading a leaked key from logs / the public web. See #442 review.
const provisionKeysEnabled = options.provisionKeys ?? false;
assertProvisionKeysCompatible({
provisionKeys: provisionKeysEnabled,
isPublic: !!options.public
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 9109b3d: coerced provisionKeysEnabled to strict === true at the createServer boundary. Both root-pod and named-pod paths now see the same value for the same input — a misconfigured 'true' / 1 / etc. falls through to false at entry, no longer activating the root branch's truthy check while skipping the named branch's strict check. Same fix as the createPodStructure pickup from the previous round, applied at the matching boundary.

Address Copilot pickup on #442. provisionKeysEnabled was derived as
options.provisionKeys ?? false, which preserves truthy non-booleans.
Combined with the strict === true check inside createPodStructure
(named-pod path), this left the two pod shapes behaving differently
for the same misconfigured input:

  options: { provisionKeys: 'true' }
  → root pod: if (provisionKeysEnabled) → truthy string → provisions
  → named pod: createPodStructure({ provisionKeys: 'true' }) →
              strict === true → does NOT provision

Coerce to a strict boolean at the createServer boundary so both
paths see the same value. A misconfigured `'true'` / `1` / etc. now
falls through to false at the entry point and the operator gets the
no-op behaviour on both pod shapes.

848/848 tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Comment thread src/server.js
Comment on lines +1030 to +1034
* `ownerKey` includes secretHex and secretKeyMultibase — needed by
* tests, present in case a future caller needs to perform a one-shot
* sign before the file is read back via WAC. **Callers must not log
* the secret.** The secret's only durable home is the on-disk file
* under /private/ (mode 0o600, owner-only WAC).
@melvincarvalho
melvincarvalho merged commit ac1223b into gh-pages May 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: --provision-keys flag for pod creation (Schnorr/Nostr/CID v1.0 identity)

2 participants