@commonpub/layer 0.19.2 → 0.20.0

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.
@@ -2,6 +2,14 @@
2
2
  // Initializes from build-time runtime config, then hydrates from /api/features
3
3
  // to pick up runtime DB overrides set via admin panel.
4
4
 
5
+ export interface IdentityFeatures {
6
+ linkRemoteAccounts: boolean;
7
+ signInWithRemote: boolean;
8
+ actingAs: boolean;
9
+ remoteInteract: boolean;
10
+ remotePublish: boolean;
11
+ }
12
+
5
13
  export interface FeatureFlags {
6
14
  content: boolean;
7
15
  social: boolean;
@@ -17,6 +25,12 @@ export interface FeatureFlags {
17
25
  admin: boolean;
18
26
  emailNotifications: boolean;
19
27
  publicApi: boolean;
28
+ /**
29
+ * Cross-instance delegated authorization. All sub-flags default false.
30
+ * Mirrors `@commonpub/config`'s `IdentityFeatures`. Phase 1b+ — see
31
+ * docs/sessions/136-cross-instance-identity-plan.md.
32
+ */
33
+ identity: IdentityFeatures;
20
34
  }
21
35
 
22
36
  let hydrated = false;
@@ -31,13 +45,28 @@ export const DEFAULT_FLAGS: FeatureFlags = {
31
45
  contests: false, events: false, learning: true, explainers: true,
32
46
  editorial: true, federation: false, admin: false, emailNotifications: false,
33
47
  publicApi: false,
48
+ identity: {
49
+ linkRemoteAccounts: false,
50
+ signInWithRemote: false,
51
+ actingAs: false,
52
+ remoteInteract: false,
53
+ remotePublish: false,
54
+ },
34
55
  };
35
56
 
36
57
  /** Build the initial flags by merging the layer's runtime config over defaults. */
37
58
  export function getInitialFlags(): FeatureFlags {
38
59
  const config = useRuntimeConfig();
39
60
  const buildFlags = (config.public.features as unknown as Partial<FeatureFlags> | undefined) ?? {};
40
- return { ...DEFAULT_FLAGS, ...buildFlags };
61
+ // Merge top-level booleans, but deep-merge `identity` so a partial
62
+ // runtime override (e.g., `{ identity: { actingAs: true } }`) lands on
63
+ // top of the defaulted sub-flags rather than replacing the whole
64
+ // nested object.
65
+ return {
66
+ ...DEFAULT_FLAGS,
67
+ ...buildFlags,
68
+ identity: { ...DEFAULT_FLAGS.identity, ...(buildFlags.identity ?? {}) },
69
+ };
41
70
  }
42
71
 
43
72
  export function useFeatures() {
@@ -56,9 +85,18 @@ export function useFeatures() {
56
85
  if (import.meta.client && !hydrated) {
57
86
  hydrated = true;
58
87
  ($fetch as Function)('/api/features')
59
- .then((dynamic: FeatureFlags) => {
88
+ .then((dynamic: Partial<FeatureFlags>) => {
60
89
  if (dynamic && typeof dynamic === 'object') {
61
- flags.value = { ...flags.value, ...dynamic };
90
+ // Deep-merge `identity` so a server response that omits some
91
+ // sub-flag doesn't blank it out at the client.
92
+ flags.value = {
93
+ ...flags.value,
94
+ ...dynamic,
95
+ identity: {
96
+ ...flags.value.identity,
97
+ ...(dynamic.identity ?? {}),
98
+ },
99
+ };
62
100
  }
63
101
  })
64
102
  .catch(() => { /* use build-time defaults on failure */ });
@@ -80,5 +118,6 @@ export function useFeatures() {
80
118
  admin: computed(() => flags.value.admin),
81
119
  emailNotifications: computed(() => flags.value.emailNotifications),
82
120
  publicApi: computed(() => flags.value.publicApi),
121
+ identity: computed(() => flags.value.identity),
83
122
  };
84
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -50,16 +50,16 @@
50
50
  "vue": "^3.4.0",
51
51
  "vue-router": "^4.3.0",
52
52
  "zod": "^4.3.6",
53
- "@commonpub/editor": "0.7.9",
54
- "@commonpub/config": "0.12.0",
55
- "@commonpub/docs": "0.6.2",
56
53
  "@commonpub/auth": "0.6.0",
57
- "@commonpub/learning": "0.5.2",
54
+ "@commonpub/docs": "0.6.2",
55
+ "@commonpub/config": "0.12.0",
56
+ "@commonpub/explainer": "0.7.12",
57
+ "@commonpub/editor": "0.7.9",
58
58
  "@commonpub/protocol": "0.9.9",
59
+ "@commonpub/learning": "0.5.2",
59
60
  "@commonpub/schema": "0.16.0",
60
- "@commonpub/ui": "0.8.5",
61
- "@commonpub/server": "2.49.0",
62
- "@commonpub/explainer": "0.7.12"
61
+ "@commonpub/server": "2.50.0",
62
+ "@commonpub/ui": "0.8.5"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@testing-library/jest-dom": "^6.9.1",
@@ -66,11 +66,30 @@ export default defineEventHandler(async (event) => {
66
66
  // Check if the current user is logged in — if so, link to their account
67
67
  const auth = event.context.auth;
68
68
  if (auth?.user) {
69
- await linkFederatedAccount(db, auth.user.id, tokenResult.user.actorUri, oauthState.instanceDomain, {
70
- preferredUsername: tokenResult.user.username,
71
- displayName: tokenResult.user.displayName ?? undefined,
72
- avatarUrl: tokenResult.user.avatarUrl ?? undefined,
73
- });
69
+ await linkFederatedAccount(
70
+ db,
71
+ auth.user.id,
72
+ tokenResult.user.actorUri,
73
+ oauthState.instanceDomain,
74
+ {
75
+ preferredUsername: tokenResult.user.username,
76
+ displayName: tokenResult.user.displayName ?? undefined,
77
+ avatarUrl: tokenResult.user.avatarUrl ?? undefined,
78
+ },
79
+ // Phase 1b: persist the access token so future delegated actions
80
+ // (Phase 4) can call the remote on this user's behalf. Encrypted
81
+ // at rest via @commonpub/infra/tokenCrypto. Scopes default to
82
+ // 'read write follow' for CommonPub↔CommonPub trust (the existing
83
+ // SSO model assumes mutual trustedInstances). Software-kind is
84
+ // 'cpub' here because this callback handles CommonPub→CommonPub;
85
+ // a Mastodon-login flow (Phase 2) will use a separate callback
86
+ // that detects software via WebFinger / megalodon.
87
+ {
88
+ accessToken: tokenResult.accessToken,
89
+ scopes: ['read', 'write', 'follow'],
90
+ softwareKind: 'cpub',
91
+ },
92
+ );
74
93
 
75
94
  return sendRedirect(event, '/settings/account?federated=linked', 302);
76
95
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Cross-instance identity — Nitro startup wiring.
3
+ *
4
+ * Runs at app init to:
5
+ *
6
+ * 1. Validate that any token-using `features.identity.*` flag has
7
+ * `CPUB_FED_TOKEN_KEY` set. Throws if misconfigured — the boot
8
+ * fails loudly rather than 500-ing partway through a real user's
9
+ * OAuth callback. Only `actingAs` is exempt (UI-only, no token I/O).
10
+ *
11
+ * 2. Register the Mastodon-API-backed FediClient factory so
12
+ * `run(event, ctx.active, action, input)` can dispatch to remote
13
+ * handlers when `ctx.active.kind === 'linked'`. Factory closes
14
+ * over the request-scoped DB handle.
15
+ *
16
+ * Plugin order: this should run early, alongside other infrastructure
17
+ * plugins. Nitro picks up plugins in alphabetical order — file is
18
+ * named `identity-startup.ts` so it sorts before app-level plugins.
19
+ *
20
+ * Phase-skip ergonomics: if no identity flag is enabled, the factory
21
+ * is registered anyway (cheap; the factory is lazy and only executes
22
+ * when `getFediClient` is called). The startup invariant only fires
23
+ * for flags that need the key.
24
+ *
25
+ * See docs/sessions/136-cross-instance-identity-plan.md.
26
+ */
27
+ import {
28
+ assertIdentityConfig,
29
+ createMastodonFediClientFactory,
30
+ setFediClientFactory,
31
+ } from '@commonpub/server';
32
+
33
+ export default defineNitroPlugin(() => {
34
+ const config = useConfig();
35
+
36
+ // Fails loudly + early if a token-using flag is on without the
37
+ // encryption key. Listed errors include the env var name to set.
38
+ assertIdentityConfig(config);
39
+
40
+ // Register the factory exactly once. Subsequent calls would replace
41
+ // (which is fine in tests; in prod this fires once per process).
42
+ setFediClientFactory(createMastodonFediClientFactory(useDB()));
43
+ });