Compare commits

...
5 Commits
Author SHA1 Message Date
Hermes 1fa5ba8baa [grade=B urn:ump:x37xj4seku3isvffxkwmahymvjijrwinndnfpubv664spur7kogq] fix(installer): remove BACKUP_DIR readonly collision that aborted every install
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
readonly BACKUP_DIR (constants) collided with the BACKUP_DIR= tunable, so every
curl|bash run died with 'BACKUP_DIR: readonly variable'. Removed the readonly
constant and resolve the default (/etc/dashcaddy/backups) at end of parse_args;
--backup-dir flag still overrides. Live file + /opt copy patched identically.
2026-10-05 18:18:22 -07:00
sami7777 0fd1bbd80b P7 people-store providerUserId dedupe [grade=B] (PR #4)
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
Judge B r1 urn:ump:bbfbsfru4nizg75wcxum7mwzhie6txhr22hw6jvz36wiihmgzmlq. Dogfood finding #3. 3414/3414.
2026-10-04 12:14:44 +02:00
Hermes Agent c430e02117 [grade=B urn:ump:bbfbsfru4nizg75wcxum7mwzhie6txhr22hw6jvz36wiihmgzmlq] P7 people store: dedupe upsert by providerUserId
CI / Test & Lint (pull_request) Canceled after 0s
CI / Security audit (pull_request) Canceled after 0s
Dogfood finding #3: upsertPerson resolved existing records by id only.
The wizard upserts a bootstrap admin AFTER a provider sync has already
created a person for the same Tailscale user (no id passed), so the same
providerUserId was duplicated into two records. Resolve existing by
providerUserId when no id is given; bootstrap Admin group + protectedAt are
preserved on the merge. New regression test; full suite 3414/3414.
2026-10-04 03:12:23 -07:00
sami7777 776cf3c4ca P7 tailscale coord-client wiring [grade=B] (PR #3)
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
Judge B r1 urn:ump:mao7vadnesmquhmgr4oeg554edt6fn3ykxyiydtfs2psoekd4s7q. Dogfood finding #2. 3413/3413.
2026-10-04 11:51:34 +02:00
Hermes Agent 5b3e637423 [grade=B urn:ump:mao7vadnesmquhmgr4oeg554edt6fn3ykxyiydtfs2psoekd4s7q] P7 tailscale provider: wire coord client via ctx delegation
CI / Test & Lint (pull_request) Canceled after 0s
CI / Security audit (pull_request) Canceled after 0s
Dogfood finding #2: createTailscaleProvider() built with coordClient=null;
nothing in prod ever called attachCoordClient() (the wiring the provider
docblock promised never landed), so wizard credential validation always
failed not_configured even with a valid stored token. Pass a delegating
proxy that fetches a fresh coord client per call via the sanctioned
ctx.tailscaleCoord.getClient pattern (token re-read from credentialManager
each call). Full suite 3413/3413.
2026-10-04 02:51:18 -07:00
4 changed files with 57 additions and 3 deletions

No files matched your search

@@ -82,6 +82,26 @@ describe('network-people-store: CRUD', () => {
expect(list).toHaveLength(1); // merged, not duplicated
});
test('upsertPerson merges by providerUserId when id is omitted (wizard after provider sync)', async () => {
const synced = await store.upsertPerson({
displayName: 'SamiAhmed7777',
providerUserId: 'ughVnuLDu621CNTRL',
});
expect(synced.groups).toEqual(['Admin']); // bootstrap identity
const fromWizard = await store.upsertPerson({
displayName: 'Sami Ahmed',
providerUserId: 'ughVnuLDu621CNTRL',
});
expect(fromWizard.id).toBe(synced.id);
expect(fromWizard.displayName).toBe('Sami Ahmed');
expect(fromWizard.groups).toEqual(['Admin']);
expect(fromWizard.protectedAt).toBe(synced.protectedAt);
const list = await store.listPeople();
expect(list).toHaveLength(1);
});
test('updatePerson patches only provided fields', async () => {
const p = await store.upsertPerson({ displayName: 'Bob', providerUserId: 'ts-2', groups: ['G'] });
const u = await store.updatePerson(p.id, { displayName: 'Bobby' });
+23 -1
View File
@@ -779,7 +779,29 @@ async function createApp() {
// Private Network wizard (DC-152). Dogfood finding 2026-10-04: the wizard
// factory needs INSTANCES keyed by mode id; without them every mode
// selection 503s provider_unavailable (unit tests inject their own map).
const p7TailscaleProvider = require('./network/providers/tailscale').createTailscaleProvider(); // eslint-disable-line global-require
// Coord client wiring (dogfood finding #2, 2026-10-04): the provider is
// built with coordClient=null and nothing ever called attachCoordClient()
// in prod (the promised routes/network.js wiring never landed), so wizard
// credential validation always failed not_configured. Delegate every coord
// method to a FRESH client per call — the sanctioned context pattern
// (ctx.tailscaleCoord.getClient re-reads the token from credentialManager
// each call, so wizard-stored tokens take effect without a restart).
const p7CoordProxy = new Proxy({}, {
get(_t, prop) {
if (typeof prop !== 'string') return undefined;
return async (...args) => {
const client = await ctx.tailscaleCoord.getClient();
const fn = client ? client[prop] : null;
if (typeof fn !== 'function') {
throw new Error('coord client method unavailable: ' + String(prop));
}
return fn.apply(client, args);
};
},
});
const p7TailscaleProvider = require('./network/providers/tailscale').createTailscaleProvider({ // eslint-disable-line global-require
coordClient: p7CoordProxy,
});
const p7WireGuardProvider = require('./network/providers/wireguard').createWireGuardProvider(); // eslint-disable-line global-require
apiRouter.use('/network/wizard', networkWizardRoutes({
registry: { getActiveProvider },
@@ -180,7 +180,17 @@ function createPeopleStore(opts = {}) {
function upsertPerson(input = {}) {
return _enqueue(() => {
const data = _load();
const existing = input.id ? data.people[input.id] : null;
// Resolve the existing record by id when given, else by providerUserId
// (dogfood finding 2026-10-04: the wizard upserts a person AFTER a
// provider sync already created one for the same Tailscale user with no
// id — without this lookup the same providerUserId was duplicated).
const existing = input.id
? data.people[input.id]
: (input.providerUserId
? Object.values(data.people).find(
(p) => p.providerUserId && p.providerUserId === input.providerUserId
) || null
: null);
const isFirst = Object.keys(data.people).length === 0;
const id = existing ? existing.id : (input.id || crypto.randomUUID());
+3 -1
View File
@@ -27,7 +27,6 @@ readonly API_DIR="${SITES_DIR}/dashcaddy-api"
readonly DASHBOARD_DIR="${SITES_DIR}/status"
readonly CONTAINER_NAME="dashcaddy-api"
readonly CADDY_ADMIN_PORT=2019
readonly BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
readonly DEFAULT_MAX_STORAGE_BYTES=""
# ---- Tunables (overridable via flags) --------------------------------------
@@ -1045,6 +1044,9 @@ parse_args() {
# Normalize TLD
if [[ -n "$TLD" && "$TLD" != .* ]]; then TLD=".$TLD"; fi
# Resolve BACKUP_DIR default when not set by the --backup-dir flag.
BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
}
print_help() {