opencode-muse-auth 0.4.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Plugin as V2Plugin, Integration } from "@opencode/plugin";
2
+ import { DEFAULT_UPSTREAM, startMuseCodeAdapter } from "./adapter.js";
2
3
  import { writeCache, deviceAuthorize, pollToken, mintKey, resolveLoaderKey, } from "./auth.js";
3
4
  const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000;
5
+ /** Origin of the real Meta inference endpoint; the adapter forwards to it. */
6
+ const META_ORIGIN = new URL(DEFAULT_UPSTREAM).origin;
4
7
  /** Keep diagnostics useful without ever copying a credential into a log line. */
5
8
  function describeError(error) {
6
9
  const text = error instanceof Error ? error.message : String(error);
@@ -25,6 +28,135 @@ async function logAuthFailure(client, error) {
25
28
  // Logging must never break the auth flow.
26
29
  }
27
30
  }
31
+ /** Start the loopback adapter; `off` keeps the proxy but skips the reshaping. */
32
+ function startLocalAdapter() {
33
+ return startMuseCodeAdapter({ passthrough: process.env["MUSE_CODE_ADAPTER"] === "off" });
34
+ }
35
+ /**
36
+ * Rewrite one direct Meta request onto the adapter origin.
37
+ *
38
+ * Only the origin changes: path, query, method, headers, body and signal are
39
+ * preserved. A request already targeting the adapter, or any other host, is
40
+ * returned untouched.
41
+ */
42
+ function routeThroughAdapter(request, baseUrl) {
43
+ const url = new URL(request.url);
44
+ const target = new URL(baseUrl);
45
+ if (url.origin === target.origin || url.origin !== META_ORIGIN)
46
+ return request;
47
+ url.protocol = target.protocol;
48
+ url.hostname = target.hostname;
49
+ url.port = target.port;
50
+ const init = {
51
+ method: request.method,
52
+ headers: request.headers,
53
+ signal: request.signal,
54
+ redirect: request.redirect,
55
+ };
56
+ if (request.method !== "GET" && request.method !== "HEAD" && request.body !== null) {
57
+ init.body = request.body;
58
+ init.duplex = "half";
59
+ }
60
+ return new Request(url.toString(), init);
61
+ }
62
+ /**
63
+ * V2 setup. Returns the cleanup function, which stops the adapter.
64
+ *
65
+ * @param options - test seam; production calls setup without options.
66
+ */
67
+ export async function setupV2(ctx, options = {}) {
68
+ const methodID = Integration.MethodID.make("muse-code-subscription");
69
+ await ctx.integration.transform((editor) => {
70
+ editor.method.update({
71
+ integrationID: "meta",
72
+ method: {
73
+ id: methodID,
74
+ type: "oauth",
75
+ label: "Muse Code subscription (Meta device login)",
76
+ },
77
+ async authorize() {
78
+ const device = await deviceAuthorize();
79
+ const url = device.verification_uri_complete || device.verification_uri;
80
+ const callback = (async () => {
81
+ try {
82
+ const access = await pollToken(device.device_code, device.interval, device.expires_in);
83
+ const minted = await mintKey(access);
84
+ return {
85
+ type: "oauth",
86
+ methodID,
87
+ access: minted.apiKey,
88
+ refresh: "",
89
+ expires: Date.now() + TEN_YEARS_MS,
90
+ metadata: { accountId: minted.accountId, ...(minted.email ? { email: minted.email } : {}) },
91
+ };
92
+ }
93
+ catch (error) {
94
+ throw new Error(`Muse Code login failed: ${describeError(error)}`);
95
+ }
96
+ })();
97
+ // The runtime awaits this promise after displaying the device code.
98
+ // Attach a handler immediately in case the exchange fails first.
99
+ void callback.catch(() => { });
100
+ return {
101
+ url,
102
+ instructions: `Open ${url}\nEnter code: ${device.user_code}`,
103
+ mode: "auto",
104
+ expiresAt: Date.now() + device.expires_in * 1000,
105
+ callback,
106
+ };
107
+ },
108
+ label: (credential) => typeof credential.metadata?.email === "string"
109
+ ? credential.metadata.email : "Muse Code subscription",
110
+ });
111
+ });
112
+ // Meta's gate inspects the declared tool names, so subscription inference
113
+ // goes through a loopback proxy that aliases the colliding names and
114
+ // restores them in the response. Failures keep the pre-adapter behavior.
115
+ let started;
116
+ try {
117
+ started = await (options.adapter ?? startLocalAdapter)();
118
+ }
119
+ catch (error) {
120
+ console.error(`opencode-muse-auth: the local request adapter could not start; ` +
121
+ `subscription requests will not be reshaped: ${describeError(error)}`);
122
+ }
123
+ const adapter = started;
124
+ async function subscription() {
125
+ const connection = await ctx.integration.connection.active("meta");
126
+ if (!connection)
127
+ return undefined;
128
+ const credential = await ctx.integration.connection.resolve(connection);
129
+ if (credential?.type !== "oauth" || credential.methodID !== methodID)
130
+ return undefined;
131
+ return credential;
132
+ }
133
+ if (adapter) {
134
+ // Primary mechanism: the provider uses the adapter's /v1 origin as its
135
+ // request base URL. Native provider drivers honor this override.
136
+ const baseUrl = adapter.baseUrl;
137
+ await ctx.session.hook("model.request", async (event) => {
138
+ if (await subscription())
139
+ event.baseURL = baseUrl;
140
+ }, { providerID: "meta" });
141
+ }
142
+ // Meta's native provider expects a bearer inference key, not the
143
+ // short-lived OAuth token used to mint it. Honor the active connection.
144
+ // AI SDK routes drop model.request.baseURL (opencode#55984), so this hook
145
+ // also rewrites the assembled request onto the adapter origin.
146
+ await ctx.session.hook("http.request", async (event) => {
147
+ const credential = await subscription();
148
+ if (!credential)
149
+ return;
150
+ event.request.headers.set("Authorization", `Bearer ${credential.access}`);
151
+ if (adapter)
152
+ event.request = routeThroughAdapter(event.request, adapter.baseUrl);
153
+ }, { providerID: "meta" });
154
+ return async () => {
155
+ await adapter?.close();
156
+ };
157
+ }
158
+ // V1 has no adapter: its inference keeps going straight to api.meta.ai and
159
+ // still receives Meta's client-restriction 403.
28
160
  const MuseCodeAuth = async ({ client }) => {
29
161
  return {
30
162
  auth: {
@@ -66,63 +198,7 @@ const MuseCodeAuth = async ({ client }) => {
66
198
  export default {
67
199
  ...V2Plugin.define({
68
200
  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
- },
201
+ setup: (ctx) => setupV2(ctx),
126
202
  }),
127
203
  server: MuseCodeAuth,
128
204
  };
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "opencode-muse-auth",
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.",
3
+ "version": "0.5.0",
4
+ "description": "Unofficial and experimental: Muse Spark in opencode using a Muse Code subscription credential, with a local loopback adapter for Meta's subscription wire format. 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",
8
8
  "license": "MIT",
9
- "author": "TheStreamCode",
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/TheStreamCode/opencode-muse-auth.git"
9
+ "author": {
10
+ "name": "Michael Gasperini",
11
+ "url": "https://mikesoft.it"
13
12
  },
14
- "homepage": "https://github.com/TheStreamCode/opencode-muse-auth#readme",
13
+ "repository": "github:TheStreamCode/opencode-muse-auth",
14
+ "homepage": "https://www.npmjs.com/package/opencode-muse-auth",
15
15
  "bugs": {
16
16
  "url": "https://github.com/TheStreamCode/opencode-muse-auth/issues"
17
17
  },
@@ -33,7 +33,8 @@
33
33
  "LICENSE",
34
34
  "NOTICE",
35
35
  "DISCLAIMER.md",
36
- "AUTH_CONTRACT.md"
36
+ "AUTH_CONTRACT.md",
37
+ "ADAPTER_CONTRACT.md"
37
38
  ],
38
39
  "scripts": {
39
40
  "build": "tsc",
@@ -42,11 +43,11 @@
42
43
  "prepublishOnly": "npm run build"
43
44
  },
44
45
  "devDependencies": {
45
- "@opencode-ai/plugin": "^1.19.0",
46
- "@types/node": "^22.20.3",
46
+ "@opencode-ai/plugin": "^1.19.3",
47
+ "@types/node": "^26.6.5",
47
48
  "typescript": "^7.0.2"
48
49
  },
49
50
  "dependencies": {
50
- "@opencode/plugin": "^2.0.27"
51
+ "@opencode/plugin": "^2.0.32"
51
52
  }
52
53
  }