offscale in the catalogue, and the comments the rename falsified

The plugin half is in gitea.officer.dev/plugins/offscale (95b84ea). This is what
the platform owed it.

- marketplace: offscale now ships assets/icon.png, so the catalogue carries an
  iconUrl and a `bare` tile like music's. The lucide `icon` stays as the fallback
  for a failed asset publish — a glyph beats the generic box.

- Four schema files cited `headscale_servers` as the shape they copied. The table
  is `offscale_servers` as of today, so those sentences named a table that no
  longer exists. Same for secret-store.ts's list of purposes.

The crypto purpose moved with it: `decryptSecret('headscale', …)` in the plugin
is now `'offscale'`, and the secret-store row was renamed rather than abandoned,
so the key material is preserved and the change is reversible. Safe only because
offscale_servers held 0 rows — one stored API key and this would have been a
re-encryption, not a rename.

Verified: offscale_servers, uq_offscale_servers_one_active and
uq_offscale_servers_user_url all exist in Postgres and nothing named headscale
does. Dropped the empty table by hand first, because drizzle-kit push treats a
rename as an interactive prompt and would have hung.

tsgo clean. 808 pass / 7 fail — +21 from offscale's first tests, same 7 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 18:42:06 +00:00
co-authored by Claude Opus 5
parent dd26e4a688
commit 640529ccba
6 changed files with 10 additions and 6 deletions
@@ -10,7 +10,7 @@ import { users } from '../auth/schema';
// instance was a shell task on the server rather than something the owner could do from the app. // instance was a shell task on the server rather than something the owner could do from the app.
// //
// It is a REGISTRY, not a single row: the owner adds any number of accounts and switches between them, the // It is a REGISTRY, not a single row: the owner adds any number of accounts and switches between them, the
// same shape headscale_servers and photos_config use. Uniqueness is on the label rather than the URL, // same shape offscale_servers and photos_config use. Uniqueness is on the label rather than the URL,
// because the same instance with a different company is a different account here, and two of those share // because the same instance with a different company is a different account here, and two of those share
// a URL AND a token. // a URL AND a token.
// //
@@ -6,7 +6,7 @@ import { users } from '../auth/schema';
// //
// A REGISTRY rather than a single row, and here that is not speculative: this machine runs four Jellyfin // A REGISTRY rather than a single row, and here that is not speculative: this machine runs four Jellyfin
// instances on separate ports (video, albums, DJ sets, and a second person's), and the owner switches // instances on separate ports (video, albums, DJ sets, and a second person's), and the owner switches
// between them. Same shape as invoiceshelf_accounts and headscale_servers. // between them. Same shape as invoiceshelf_accounts and offscale_servers.
// //
// `accessToken` is encrypted at rest via ../crypto.ts. A Jellyfin access token can read every item, every // `accessToken` is encrypted at rest via ../crypto.ts. A Jellyfin access token can read every item, every
// watch history and every stream on the instance, so a DB dump must not hand it over. Encryption is confined // watch history and every stream on the instance, so a DB dump must not hand it over. Encryption is confined
@@ -10,7 +10,7 @@ import { users } from '../auth/schema';
// than something the owner could do from the app. // than something the owner could do from the app.
// //
// It is a REGISTRY, not a single row: the owner adds any number of accounts and switches between them, the // It is a REGISTRY, not a single row: the owner adds any number of accounts and switches between them, the
// same shape headscale_servers uses. Two accounts on the same instance is the normal case (one key per // same shape offscale_servers uses. Two accounts on the same instance is the normal case (one key per
// Immich user), which is why the uniqueness below is on the label and not on the URL. // Immich user), which is why the uniqueness below is on the label and not on the URL.
// //
// `api_key` is encrypted at rest via ../crypto.ts. An Immich key can read and delete the entire library, so a // `api_key` is encrypted at rest via ../crypto.ts. An Immich key can read and delete the entire library, so a
+1 -1
View File
@@ -117,7 +117,7 @@ function readActive(purpose: string): string | null {
/** /**
* The active key for a purpose, created on first use. * The active key for a purpose, created on first use.
* *
* Purposes are plain strings and belong to whoever owns the data they protect: `jwt`, `headscale`, * Purposes are plain strings and belong to whoever owns the data they protect: `jwt`, `offscale`,
* `wallet`, `photos`, `jellyfin`, `invoiceshelf`, `vault`, `service-connections`. A plugin asks for its * `wallet`, `photos`, `jellyfin`, `invoiceshelf`, `vault`, `service-connections`. A plugin asks for its
* own and never another's — same rule `service_connections` rows already follow. * own and never another's — same rule `service_connections` rows already follow.
* *
@@ -9,7 +9,7 @@ import { users } from '../auth/schema';
// TWO COLUMNS HOLD SPENDING AUTHORITY AND THEY ARE PROTECTED DIFFERENTLY. This asymmetry is deliberate: // TWO COLUMNS HOLD SPENDING AUTHORITY AND THEY ARE PROTECTED DIFFERENTLY. This asymmetry is deliberate:
// //
// `config` — node credentials (macaroon, rune, LNDHub password, NWC URI). Encrypted at rest with // `config` — node credentials (macaroon, rune, LNDHub password, NWC URI). Encrypted at rest with
// the 'wallet' store key via ../crypto.ts, the way headscale_servers.api_key uses its // the 'wallet' store key via ../crypto.ts, the way offscale_servers.api_key uses its
// own. It CANNOT be // own. It CANNOT be
// passphrase-protected: background balance polling needs it without the owner present. // passphrase-protected: background balance polling needs it without the owner present.
// //
@@ -58,7 +58,7 @@ export const walletWallets = pgTable(
(t) => [ (t) => [
uniqueIndex('uq_wallet_wallets_user_name').on(t.userId, t.name), uniqueIndex('uq_wallet_wallets_user_name').on(t.userId, t.name),
// At most one active wallet per owner, enforced by the DB rather than convention — a partial unique // At most one active wallet per owner, enforced by the DB rather than convention — a partial unique
// index over active rows only, mirroring uq_headscale_servers_one_active. // index over active rows only, mirroring uq_offscale_servers_one_active.
uniqueIndex('uq_wallet_wallets_one_active') uniqueIndex('uq_wallet_wallets_one_active')
.on(t.userId) .on(t.userId)
.where(sql`${t.isActive}`), .where(sql`${t.isActive}`),
+4
View File
@@ -124,6 +124,10 @@ const CATALOGUE: CatalogueEntry[] = [
label: 'Offscale', label: 'Offscale',
summary: 'Your tailnet — machines, users, pre-auth keys, access policy and device invites', summary: 'Your tailnet — machines, users, pre-auth keys, access policy and device invites',
color: '#818cf8', color: '#818cf8',
// Ships artwork as of 2026-08-15, so the tile is `bare` like music's. `icon` stays as the fallback
// for the case the assets fail to publish — the glyph is strictly better than the generic box.
tile: 'bare',
iconUrl: 'https://gitea.officer.dev/plugins/offscale/raw/branch/main/assets/icon.png',
icon: 'Network', icon: 'Network',
source: 'https://gitea.officer.dev/plugins/offscale.git', source: 'https://gitea.officer.dev/plugins/offscale.git',
permissions: [ permissions: [