opencode-muse-auth 0.2.0 → 0.4.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.
@@ -0,0 +1,32 @@
1
+ # Muse auth contract (v1)
2
+
3
+ The three Muse Code plugins implement the same Meta device-code flow. Change
4
+ this file and the pinned constants/tests together in all three repositories:
5
+
6
+ - `opencode-muse-auth`
7
+ - `hermes-muse-code`
8
+ - `openclaw-muse-code`
9
+
10
+ ## Wire contract
11
+
12
+ | Item | Value |
13
+ |---|---|
14
+ | `CLIENT_ID` | `1031625952748946` |
15
+ | Device authorization | `https://auth.meta.com/oidc/device/authorization/` |
16
+ | Device token | `https://auth.meta.com/oidc/device/token/` |
17
+ | Key mint | `https://api.meta.ai/muse-code/key` |
18
+ | API version header | `x-api-version: 1.0.0` |
19
+ | Cache fields | `apiKey`, `accountId`, `email` (never the OAuth access token) |
20
+ | Inference base URL | `https://api.meta.ai/v1` |
21
+
22
+ ## Behaviour
23
+
24
+ - Device flow: request a device code, poll the token endpoint, accept
25
+ `authorization_pending`, increase the interval on `slow_down`, stop on a
26
+ terminal error or at `expires_in`.
27
+ - Key mint: exchange the OAuth access token for the account-bound inference
28
+ key; reject an inactive subscription or a pending payment.
29
+ - Persistence: only the inference key and account identity are stored; the
30
+ OAuth access token stays in memory.
31
+ - Any change to this contract bumps `AUTH_CONTRACT_VERSION` in the code and
32
+ requires updating the pinned tests in all three repositories.
package/DISCLAIMER.md ADDED
@@ -0,0 +1,62 @@
1
+ # Disclaimer and risk notice
2
+
3
+ This project is an unofficial, community-built integration. It is not affiliated
4
+ with, endorsed by, or supported by Meta Platforms, Inc. or its affiliates
5
+ ("Meta"). "Meta", "Muse", "Muse Code", "Muse Spark" and related names are
6
+ trademarks of Meta.
7
+
8
+ ## Why this notice exists
9
+
10
+ This plugin signs in with a Meta account that has an active Muse Code
11
+ subscription and uses the subscription-linked credential to send inference
12
+ requests from opencode, a third-party coding client.
13
+
14
+ Meta's [Meta Model API Terms of Service](https://dev.meta.ai/legal/terms-of-service),
15
+ Section 3.2, states that a credential connected for a Coding Harness
16
+ Subscription "is for use only with the coding harness under your Coding Harness
17
+ Subscription" and "may not be used to access the Services outside the coding
18
+ harness or to circumvent your subscription's usage limits or billing". Meta's
19
+ [Muse Code subscription documentation](https://dev.meta.ai/docs/muse-code/subscriptions)
20
+ likewise states that the subscription credential "is for use with Muse Code
21
+ only".
22
+
23
+ This project therefore relies on a use that Meta does not document as supported.
24
+ Meta may change, restrict or enforce its terms at any time, and may suspend,
25
+ restrict or terminate accounts, subscriptions or credentials involved. The
26
+ authors cannot prevent, predict or reverse that outcome.
27
+
28
+ ## No warranty, no guarantee
29
+
30
+ - The integration is provided "as is"; there is no guarantee that it is
31
+ permitted by Meta's terms, that it will keep working, or that the subscription
32
+ will remain available.
33
+ - The authors cannot restore an account, subscription, credential or data after
34
+ Meta takes action, and cannot intervene with Meta on your behalf.
35
+ - This notice is informational and is not legal advice.
36
+
37
+ ## Your responsibility
38
+
39
+ By installing or using this software you acknowledge that:
40
+
41
+ 1. you are responsible for complying with Meta's terms, including Section 3.2;
42
+ 2. you use your own account and credentials and accept the risk of account or
43
+ subscription action by Meta;
44
+ 3. you will not use this software where losing access would be unacceptable, and
45
+ you will not share credential caches or exported keys;
46
+ 4. you will delete cached credentials when you stop using the software.
47
+
48
+ ## Limitation of liability
49
+
50
+ To the maximum extent permitted by applicable law, the authors and contributors
51
+ are not liable for any loss or damage arising out of or related to this software
52
+ or its use, including account suspension, subscription termination, credential
53
+ revocation, loss of data, or service interruption. Nothing in this notice
54
+ excludes or limits liability that cannot be excluded or limited by applicable
55
+ law. The [MIT License](LICENSE) contains the warranty disclaimer and liability
56
+ limitation that govern the software; this notice does not modify it.
57
+
58
+ ## Clearly supported alternatives
59
+
60
+ If you need an integration that is unambiguously within Meta's terms, use a Meta
61
+ Model API key billed pay-as-you-go, or drive Muse Code through its official
62
+ `muse serve` session interface.
package/README.md CHANGED
@@ -3,13 +3,19 @@
3
3
  Use Meta **Muse Spark** in [opencode](https://opencode.ai) billed to your
4
4
  **Muse Code monthly subscription** — no pay-as-you-go API key, no third-party CLI.
5
5
 
6
+ > **Unofficial and experimental.** This plugin uses a Muse Code subscription
7
+ > credential from opencode. Meta's terms limit that credential to Muse Code, so
8
+ > this use may not be supported and Meta may suspend or revoke accounts or
9
+ > credentials. Use it at your own risk; the authors cannot recover an account.
10
+ > Read [DISCLAIMER.md](DISCLAIMER.md) before installing.
11
+
6
12
  ## Install
7
13
 
8
14
  ```jsonc
9
15
  // opencode.json
10
16
  {
11
17
  "$schema": "https://opencode.ai/config.json",
12
- "plugin": ["opencode-muse-auth"]
18
+ "plugins": ["opencode-muse-auth"]
13
19
  }
14
20
  ```
15
21
 
@@ -19,7 +25,7 @@ Enter the code in your browser (your own Meta account) and the login
19
25
  completes by itself.
20
26
 
21
27
  Verified end-to-end against the published package: isolated project with
22
- only `"plugin": ["opencode-muse-auth"]`, live chat on `meta/muse-spark-1.3`
28
+ only `"plugins": ["opencode-muse-auth"]`, live chat on `meta/muse-spark-1.3`
23
29
  billed to subscription.
24
30
 
25
31
  ## Requirements
@@ -35,16 +41,32 @@ automatically to `muse-*` model ids):
35
41
 
36
42
  - **Login** — Meta device-code exchange (RFC 8628) inside `/connect`, then
37
43
  mints the stable account-bound inference key via the Model API. The key is
38
- cached locally (`~/.config/opencode/muse-code-sub.json`, owner-only
39
- permissions where supported) and never printed. The cache stores exactly
40
- `apiKey`, `accountId`, and `email` — the OAuth access token from the
41
- login flow is kept in memory only and never written to disk.
42
- - **Runtime** — an auth `loader` injects the cached key on every startup.
43
- The mint endpoint is aggressively rate-limited, so the plugin never
44
- re-mints on its own; re-run `/connect` only if access is revoked (401).
44
+ never printed and the OAuth access token from the login flow is kept in
45
+ memory only.
46
+ - **V1 storage** — OpenCode's auth store is the primary source; the auth
47
+ `loader` falls back to the legacy cache
48
+ (`~/.config/opencode/muse-code-sub.json`) only when the store has no key.
49
+ The cache is still written so upgrades keep working.
50
+ - **V2 storage** — the credential lives in OpenCode's integration store; the
51
+ plugin does not write the local cache on this path.
52
+ - **Runtime** — the mint endpoint is aggressively rate-limited, so the plugin
53
+ never re-mints on its own; re-run `/connect` only if access is revoked (401).
45
54
 
46
55
  Flow parameters are compatible with the published behavior of oh-my-pi (MIT);
47
- see [NOTICE](NOTICE).
56
+ see [NOTICE](NOTICE) and [AUTH_CONTRACT.md](AUTH_CONTRACT.md).
57
+
58
+ ## Credentials and removal
59
+
60
+ The inference key is held by OpenCode (V1 auth store or V2 integration store);
61
+ the legacy cache, when present, stores only `apiKey`, `accountId`, and `email`.
62
+ The OAuth access token is never written to disk. To remove access:
63
+
64
+ - disconnect the provider or log out in OpenCode (this removes the key OpenCode
65
+ uses; the store is `~/.local/share/opencode/auth.json`);
66
+ - delete `~/.config/opencode/muse-code-sub.json` if it exists from an older
67
+ version;
68
+ - run `muse logout` if you also want to clear the Muse Code CLI session;
69
+ - remove `opencode-muse-auth` from `opencode.json`.
48
70
 
49
71
  ## Development
50
72
 
@@ -56,4 +78,5 @@ npm test
56
78
 
57
79
  ## License
58
80
 
59
- MIT — see [LICENSE](LICENSE).
81
+ MIT — see [LICENSE](LICENSE). The software is provided "as is"; see
82
+ [DISCLAIMER.md](DISCLAIMER.md) for the risk notice and liability terms.
package/dist/auth.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const CLIENT_ID = "1031625952748946";
2
2
  export declare const DEVICE_URL = "https://auth.meta.com/oidc/device/authorization/";
3
3
  export declare const TOKEN_URL = "https://auth.meta.com/oidc/device/token/";
4
4
  export declare const KEY_URL = "https://api.meta.ai/muse-code/key";
5
+ export declare const AUTH_CONTRACT_VERSION = 1;
5
6
  export declare const CACHE_PATH: string;
6
7
  export interface DeviceAuthorization {
7
8
  device_code: string;
@@ -24,6 +25,13 @@ export interface CachedCredential {
24
25
  email?: string;
25
26
  }
26
27
  export declare function readCache(path?: string): Promise<string>;
28
+ /**
29
+ * Resolve the provider key for the V1 auth loader.
30
+ *
31
+ * OpenCode's auth store is authoritative; the local cache is a legacy fallback
32
+ * for installs created before the host store became the primary source.
33
+ */
34
+ export declare function resolveLoaderKey(auth: () => Promise<unknown>, readCacheFn?: () => Promise<string>): Promise<string>;
27
35
  export declare function writeCache(credentials: MintedCredential, path?: string): Promise<void>;
28
36
  export declare function deviceAuthorize(): Promise<DeviceAuthorization>;
29
37
  export declare function pollToken(deviceCode: string, intervalS: number, expiresInS: number): Promise<string>;
package/dist/auth.js CHANGED
@@ -8,6 +8,7 @@ export const CLIENT_ID = "1031625952748946";
8
8
  export const DEVICE_URL = "https://auth.meta.com/oidc/device/authorization/";
9
9
  export const TOKEN_URL = "https://auth.meta.com/oidc/device/token/";
10
10
  export const KEY_URL = "https://api.meta.ai/muse-code/key";
11
+ export const AUTH_CONTRACT_VERSION = 1;
11
12
  export const CACHE_PATH = join(homedir(), ".config", "opencode", "muse-code-sub.json");
12
13
  const HEADERS = {
13
14
  Accept: "application/json",
@@ -25,6 +26,27 @@ export async function readCache(path = CACHE_PATH) {
25
26
  return "";
26
27
  }
27
28
  }
29
+ /**
30
+ * Resolve the provider key for the V1 auth loader.
31
+ *
32
+ * OpenCode's auth store is authoritative; the local cache is a legacy fallback
33
+ * for installs created before the host store became the primary source.
34
+ */
35
+ export async function resolveLoaderKey(auth, readCacheFn = readCache) {
36
+ try {
37
+ const stored = await auth();
38
+ if (stored && typeof stored === "object") {
39
+ const candidate = stored;
40
+ if (candidate.type === "api" && typeof candidate.key === "string" && candidate.key.trim()) {
41
+ return candidate.key.trim();
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ // Fall back to the legacy cache below.
47
+ }
48
+ return readCacheFn();
49
+ }
28
50
  export async function writeCache(credentials, path = CACHE_PATH) {
29
51
  // Retention minimization: persist only what inference needs. The OAuth
30
52
  // access token has unknown broader scope and nothing reads it back, so it
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
+ import { Plugin as V2Plugin } from "@opencode/plugin";
2
3
  declare const _default: {
3
4
  id: string;
5
+ setup: (context: V2Plugin.Context) => Promise<V2Plugin.Cleanup | void> | V2Plugin.Cleanup | void;
4
6
  server: Plugin;
5
- setup(): void;
6
7
  };
7
8
  export default _default;
package/dist/index.js CHANGED
@@ -1,5 +1,31 @@
1
- import { readCache, writeCache, deviceAuthorize, pollToken, mintKey } from "./auth.js";
2
- const MuseCodeAuth = async () => {
1
+ import { Plugin as V2Plugin, Integration } from "@opencode/plugin";
2
+ import { writeCache, deviceAuthorize, pollToken, mintKey, resolveLoaderKey, } from "./auth.js";
3
+ const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000;
4
+ /** Keep diagnostics useful without ever copying a credential into a log line. */
5
+ function describeError(error) {
6
+ const text = error instanceof Error ? error.message : String(error);
7
+ return text
8
+ .replace(/Bearer\s+\S+/gi, "Bearer ***")
9
+ .replace(/LLM\|[^\s"']+/g, "LLM|***")
10
+ .slice(0, 300);
11
+ }
12
+ async function logAuthFailure(client, error) {
13
+ try {
14
+ const candidate = client;
15
+ await candidate?.app?.log?.({
16
+ body: {
17
+ service: "opencode-muse-auth",
18
+ level: "error",
19
+ message: "Muse Code login failed",
20
+ extra: { error: describeError(error) },
21
+ },
22
+ });
23
+ }
24
+ catch {
25
+ // Logging must never break the auth flow.
26
+ }
27
+ }
28
+ const MuseCodeAuth = async ({ client }) => {
3
29
  return {
4
30
  auth: {
5
31
  provider: "meta",
@@ -21,7 +47,8 @@ const MuseCodeAuth = async () => {
21
47
  await writeCache(minted);
22
48
  return { type: "success", key: minted.apiKey };
23
49
  }
24
- catch {
50
+ catch (error) {
51
+ await logAuthFailure(client, error);
25
52
  return { type: "failed" };
26
53
  }
27
54
  },
@@ -29,15 +56,73 @@ const MuseCodeAuth = async () => {
29
56
  },
30
57
  },
31
58
  ],
32
- loader: async () => {
33
- const key = await readCache();
59
+ loader: async (auth) => {
60
+ const key = await resolveLoaderKey(auth);
34
61
  return key ? { apiKey: key } : {};
35
62
  },
36
63
  },
37
64
  };
38
65
  };
39
66
  export default {
40
- id: "muse-code-auth",
67
+ ...V2Plugin.define({
68
+ id: "muse-code-auth",
69
+ async setup(ctx) {
70
+ const methodID = Integration.MethodID.make("muse-code-subscription");
71
+ await ctx.integration.transform((editor) => {
72
+ editor.method.update({
73
+ integrationID: "meta",
74
+ method: {
75
+ id: methodID,
76
+ type: "oauth",
77
+ label: "Muse Code subscription (Meta device login)",
78
+ },
79
+ async authorize() {
80
+ const device = await deviceAuthorize();
81
+ const url = device.verification_uri_complete || device.verification_uri;
82
+ const callback = (async () => {
83
+ try {
84
+ const access = await pollToken(device.device_code, device.interval, device.expires_in);
85
+ const minted = await mintKey(access);
86
+ return {
87
+ type: "oauth",
88
+ methodID,
89
+ access: minted.apiKey,
90
+ refresh: "",
91
+ expires: Date.now() + TEN_YEARS_MS,
92
+ metadata: { accountId: minted.accountId, ...(minted.email ? { email: minted.email } : {}) },
93
+ };
94
+ }
95
+ catch (error) {
96
+ throw new Error(`Muse Code login failed: ${describeError(error)}`);
97
+ }
98
+ })();
99
+ // The runtime awaits this promise after displaying the device code.
100
+ // Attach a handler immediately in case the exchange fails first.
101
+ void callback.catch(() => { });
102
+ return {
103
+ url,
104
+ instructions: `Open ${url}\nEnter code: ${device.user_code}`,
105
+ mode: "auto",
106
+ expiresAt: Date.now() + device.expires_in * 1000,
107
+ callback,
108
+ };
109
+ },
110
+ label: (credential) => typeof credential.metadata?.email === "string"
111
+ ? credential.metadata.email : "Muse Code subscription",
112
+ });
113
+ });
114
+ // Meta's native provider expects a bearer inference key, not the
115
+ // short-lived OAuth token used to mint it. Honor the active connection.
116
+ await ctx.session.hook("http.request", async (event) => {
117
+ const connection = await ctx.integration.connection.active("meta");
118
+ if (!connection)
119
+ return;
120
+ const credential = await ctx.integration.connection.resolve(connection);
121
+ if (credential?.type === "oauth" && credential.methodID === methodID) {
122
+ event.request.headers.set("Authorization", `Bearer ${credential.access}`);
123
+ }
124
+ }, { providerID: "meta" });
125
+ },
126
+ }),
41
127
  server: MuseCodeAuth,
42
- setup() { },
43
128
  };
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // OpenCode V2 resolves local directories through their root entrypoint.
2
+ export { default } from "./dist/index.js"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-muse-auth",
3
- "version": "0.2.0",
4
- "description": "Muse Spark in opencode billed to the Muse Code monthly subscription (Meta device login, no API key)",
3
+ "version": "0.4.0",
4
+ "description": "Unofficial and experimental: Muse Spark in opencode using a Muse Code subscription credential. May conflict with Meta's terms; use at your own risk. See DISCLAIMER.md.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "type": "module",
@@ -27,10 +27,13 @@
27
27
  "plugin"
28
28
  ],
29
29
  "files": [
30
+ "index.js",
30
31
  "dist",
31
32
  "README.md",
32
33
  "LICENSE",
33
- "NOTICE"
34
+ "NOTICE",
35
+ "DISCLAIMER.md",
36
+ "AUTH_CONTRACT.md"
34
37
  ],
35
38
  "scripts": {
36
39
  "build": "tsc",
@@ -42,5 +45,8 @@
42
45
  "@opencode-ai/plugin": "^1.19.0",
43
46
  "@types/node": "^22.20.3",
44
47
  "typescript": "^7.0.2"
48
+ },
49
+ "dependencies": {
50
+ "@opencode/plugin": "^2.0.27"
45
51
  }
46
52
  }