@o3co/create-auth-provider 0.4.1 → 0.5.1

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,184 @@
1
+ /*
2
+ * Copyright 2026 1o1 Co. Ltd.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import path from "node:path";
17
+ import {
18
+ type AppConfig,
19
+ createFederationTokenStoreFactory,
20
+ createInMemorySessionFamilyIndex,
21
+ createInMemorySessionFederationIndex,
22
+ createInMemorySessionRPRegistry,
23
+ createInMemoryUserSessionStore,
24
+ createKeyStoreFactory,
25
+ createRepositoryFactories,
26
+ defineModule,
27
+ type Module,
28
+ registerBuiltinFederationTokenStores,
29
+ registerBuiltinKeyStores,
30
+ } from "@o3co/auth-provider-core";
31
+ import type { GoogleProviderConfig } from "@o3co/auth-provider-federation-google";
32
+ import { registerBuiltinAdapters } from "@o3co/auth-provider-foundation";
33
+ import {
34
+ redisCodeRepositoryBuilder,
35
+ redisFederationTokenStoreBuilder,
36
+ } from "@o3co/auth-provider-redis";
37
+ import { extractFederationSection } from "@o3co/auth-provider-session";
38
+
39
+ /**
40
+ * Helper: turn a v0.4.x { type, [type]: {...} } adapter-config slice into the
41
+ * flat `{ type, ...rest }` shape consumed by AdapterFactory<T>.create().
42
+ *
43
+ * Composition-root concern; not a library export.
44
+ */
45
+ function flattenAdapterConfig(
46
+ section: ({ type: string } | { provider: string }) & Record<string, unknown>,
47
+ ): { type: string } & Record<string, unknown> {
48
+ const selector =
49
+ (section as { type?: string; provider?: string }).type ??
50
+ (section as { provider?: string }).provider;
51
+ if (typeof selector !== "string") {
52
+ throw new TypeError("flattenAdapterConfig: section requires 'type' or 'provider' string");
53
+ }
54
+ const sub = section[selector];
55
+ const flattenedSub =
56
+ typeof sub === "object" && sub !== null && !Array.isArray(sub)
57
+ ? (sub as Record<string, unknown>)
58
+ : {};
59
+ return { type: selector, ...flattenedSub };
60
+ }
61
+
62
+ /**
63
+ * KeyStore module — provides the JWT signing KeyStore from config.
64
+ *
65
+ * Per A2-γ §4 worked example. Composition-root-local: the standalone template
66
+ * uses the built-in local/jwks adapters; alternative deployments wire their
67
+ * own KeyStore via a different module of the same shape.
68
+ */
69
+ export const keyStoreModule: Module = defineModule({
70
+ name: "standalone:key-store",
71
+ requires: ["config"] as const,
72
+ provides: {
73
+ keyStore: async ({ config }) => {
74
+ const factory = createKeyStoreFactory();
75
+ registerBuiltinKeyStores(factory);
76
+ return factory.create(flattenAdapterConfig((config as AppConfig).oauth.jwt.signingKey));
77
+ },
78
+ },
79
+ });
80
+
81
+ /**
82
+ * Repositories module — provides client / user / code repositories from
83
+ * `config.repositories.*` slices using the built-in adapter factories.
84
+ */
85
+ export const repositoriesModule: Module = defineModule({
86
+ name: "standalone:repositories",
87
+ requires: ["config"] as const,
88
+ provides: {
89
+ clientRepository: async ({ config }) => {
90
+ const { clientFactory, userFactory } = createRepositoryFactories();
91
+ registerBuiltinAdapters({ userFactory });
92
+ const slice = flattenAdapterConfig(
93
+ (config as AppConfig).repositories.client as { type: string } & Record<string, unknown>,
94
+ );
95
+ if (typeof slice.path === "string") {
96
+ slice.path = path.resolve(process.cwd(), slice.path);
97
+ }
98
+ return clientFactory.create(slice);
99
+ },
100
+ userRepository: async ({ config }) => {
101
+ const { userFactory } = createRepositoryFactories();
102
+ registerBuiltinAdapters({ userFactory });
103
+ return userFactory.create(
104
+ flattenAdapterConfig(
105
+ (config as AppConfig).repositories.user as { type: string } & Record<string, unknown>,
106
+ ),
107
+ );
108
+ },
109
+ codeRepository: async ({ config }) => {
110
+ const { userFactory, codeFactory } = createRepositoryFactories();
111
+ registerBuiltinAdapters({ userFactory });
112
+ codeFactory.register("redis", redisCodeRepositoryBuilder);
113
+ return codeFactory.create(
114
+ flattenAdapterConfig(
115
+ (config as AppConfig).repositories.code as { type: string } & Record<string, unknown>,
116
+ ),
117
+ );
118
+ },
119
+ },
120
+ });
121
+
122
+ /**
123
+ * Stores module — provides the four-store user-session split + federation
124
+ * token store. The standalone template uses in-memory stores by default; a
125
+ * redis-backed deployment swaps this module for `@o3co/auth-provider-redis`'s
126
+ * equivalent (per A4 §10 / Phase 5).
127
+ */
128
+ export const storesModule: Module = defineModule({
129
+ name: "standalone:stores",
130
+ requires: ["config"] as const,
131
+ provides: {
132
+ userSessionStore: () => createInMemoryUserSessionStore(),
133
+ sessionRPRegistry: () => createInMemorySessionRPRegistry(),
134
+ sessionFamilyIndex: () => createInMemorySessionFamilyIndex(),
135
+ sessionFederationIndex: () => createInMemorySessionFederationIndex(),
136
+ federationTokenStore: async ({ config }) => {
137
+ const factory = createFederationTokenStoreFactory();
138
+ registerBuiltinFederationTokenStores(factory);
139
+ factory.register("redis", redisFederationTokenStoreBuilder);
140
+ const slice = (
141
+ config as AppConfig & {
142
+ federationTokenStore?: { type: string } & Record<string, unknown>;
143
+ }
144
+ ).federationTokenStore;
145
+ return factory.create(slice ? flattenAdapterConfig(slice) : { type: "memory" });
146
+ },
147
+ },
148
+ });
149
+
150
+ /**
151
+ * Google federation config bridge — supplies the typed `googleFederationConfig`
152
+ * ComponentMap slot from the `config.federations.google` slice.
153
+ *
154
+ * Per `@o3co/auth-provider-federation-google` README. Per-federation modules
155
+ * (Phase 7 A5) consume this slot; the bridge is the standalone composition
156
+ * root's responsibility because the slot's content is consumer-specific.
157
+ */
158
+ export const googleFederationConfigModule: Module = defineModule({
159
+ name: "standalone:google-federation-config",
160
+ requires: ["config"] as const,
161
+ provides: {
162
+ googleFederationConfig: ({ config }): GoogleProviderConfig => {
163
+ const slice = extractFederationSection((config as AppConfig).federations, "google");
164
+ if (!slice) {
165
+ throw new Error(
166
+ "federations.google must be enabled with credentials when googleFederationModule is in the manifest",
167
+ );
168
+ }
169
+ const clientId = slice.clientId;
170
+ const clientSecret = slice.clientSecret;
171
+ const callbackURL = slice.callbackURL;
172
+ if (
173
+ typeof clientId !== "string" ||
174
+ typeof clientSecret !== "string" ||
175
+ typeof callbackURL !== "string"
176
+ ) {
177
+ throw new Error(
178
+ "federations.google requires clientId, clientSecret, callbackURL when enabled",
179
+ );
180
+ }
181
+ return { clientId, clientSecret, callbackURL };
182
+ },
183
+ },
184
+ });
@@ -172,8 +172,8 @@ describe("POST /oauth/token", () => {
172
172
  expect(res.status).toBe(400);
173
173
  });
174
174
 
175
- it("returns 400 for grant_type=authorization without a code", async () => {
176
- const res = await client.post("/oauth/token", "grant_type=authorization", {
175
+ it("returns 400 for grant_type=authorization_code without a code", async () => {
176
+ const res = await client.post("/oauth/token", "grant_type=authorization_code", {
177
177
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
178
178
  });
179
179
  expect(res.status).toBe(400);
@@ -7,7 +7,6 @@
7
7
  "test": "vitest run"
8
8
  },
9
9
  "dependencies": {
10
- "@noble/ed25519": "^3.0.1",
11
10
  "axios": "^1.6.0",
12
11
  "jsonwebtoken": "^9.0.3"
13
12
  },
@@ -1,6 +1,17 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
3
2
  "compilerOptions": {
3
+ "target": "esnext",
4
+ "lib": ["esnext"],
5
+ "module": "nodenext",
6
+ "moduleResolution": "nodenext",
7
+ "declarationMap": true,
8
+ "declaration": true,
9
+ "emitDeclarationOnly": false,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "allowArbitraryExtensions": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
4
15
  "rootDir": "./src",
5
16
  "outDir": "./dist",
6
17
  "paths": {
@@ -1,9 +1,10 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.0.0",
3
- "@o3co/auth-provider-did": "0.0.0",
4
- "@o3co/auth-provider-federation-google": "0.0.0",
5
- "@o3co/auth-provider-federation-github": "0.0.0",
6
- "@o3co/auth-provider-oauth": "0.0.0",
7
- "@o3co/auth-provider-session": "0.0.0",
8
- "@o3co/auth-provider-foundation": "0.0.0"
2
+ "@o3co/auth-provider-core": "0.5.1",
3
+ "@o3co/auth-provider-federation-google": "0.5.1",
4
+ "@o3co/auth-provider-federation-github": "0.5.1",
5
+ "@o3co/auth-provider-oauth": "0.5.1",
6
+ "@o3co/auth-provider-oauth-token-exchange": "0.5.1",
7
+ "@o3co/auth-provider-session": "0.5.1",
8
+ "@o3co/auth-provider-foundation": "0.5.1",
9
+ "@o3co/auth-provider-redis": "0.5.1"
9
10
  }
@@ -1,193 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- import crypto from "node:crypto";
18
- import * as ed from "@noble/ed25519";
19
- import axios from "axios";
20
- import { describe, expect, it } from "vitest";
21
-
22
- const BASE_URL = process.env.API_BASE_URL || "http://localhost:3000";
23
-
24
- const client = axios.create({
25
- baseURL: BASE_URL,
26
- validateStatus: () => true,
27
- });
28
-
29
- // Helper: generate a valid DID auth request body
30
- const createDidAuthRequest = async (overrides = {}) => {
31
- const privateKey = ed.utils.randomPrivateKey();
32
- const publicKey = await ed.getPublicKeyAsync(privateKey);
33
-
34
- const did = "did:example:registry.example.com:org:test";
35
- const nonce = crypto.randomUUID();
36
- const timestamp = new Date().toISOString();
37
- const audience = "https://api.example.com";
38
-
39
- const message = JSON.stringify({ did, timestamp, nonce, audience });
40
- const messageBytes = new TextEncoder().encode(message);
41
- const signature = await ed.signAsync(messageBytes, privateKey);
42
-
43
- return {
44
- grant_type: "did",
45
- did,
46
- signature: Buffer.from(signature).toString("base64"),
47
- message,
48
- publicKey: Buffer.from(publicKey).toString("base64"),
49
- // Expose internals for test manipulation
50
- _privateKey: privateKey,
51
- _publicKey: publicKey,
52
- _nonce: nonce,
53
- ...overrides,
54
- };
55
- };
56
-
57
- describe("POST /oauth/token grant_type=did", () => {
58
- it("returns a JWT access token for a valid DID authentication", async () => {
59
- const body = await createDidAuthRequest();
60
- const res = await client.post("/oauth/token", body);
61
-
62
- expect(res.status).toBe(200);
63
- expect(res.data.access_token).toBeDefined();
64
- expect(res.data.token_type).toBe("Bearer");
65
- // M2M: no refresh token
66
- expect(res.data.refresh_token).toBeUndefined();
67
- });
68
-
69
- it("returns 401 for an invalid signature", async () => {
70
- const body = await createDidAuthRequest();
71
- // Corrupt the signature
72
- body.signature = Buffer.from(
73
- "invalid-signature-data-that-is-long-enough-to-be-64-bytes-padding!!",
74
- ).toString("base64");
75
-
76
- const res = await client.post("/oauth/token", body);
77
- expect(res.status).toBe(401);
78
- expect(res.data.error).toBe("invalid_grant");
79
- });
80
-
81
- it("returns 400 for an expired timestamp", async () => {
82
- const privateKey = ed.utils.randomPrivateKey();
83
- const publicKey = await ed.getPublicKeyAsync(privateKey);
84
-
85
- const did = "did:example:registry.example.com:org:test";
86
- // Timestamp 10 minutes ago (exceeds 5 minute max age)
87
- const timestamp = new Date(Date.now() - 10 * 60 * 1000).toISOString();
88
- const nonce = crypto.randomUUID();
89
-
90
- const message = JSON.stringify({ did, timestamp, nonce });
91
- const messageBytes = new TextEncoder().encode(message);
92
- const signature = await ed.signAsync(messageBytes, privateKey);
93
-
94
- const res = await client.post("/oauth/token", {
95
- grant_type: "did",
96
- did,
97
- signature: Buffer.from(signature).toString("base64"),
98
- message,
99
- publicKey: Buffer.from(publicKey).toString("base64"),
100
- });
101
-
102
- expect(res.status).toBe(400);
103
- expect(res.data.error).toBe("invalid_request");
104
- expect(res.data.error_description).toContain("timestamp");
105
- });
106
-
107
- it("returns 400 on nonce replay (same nonce used twice)", async () => {
108
- const body = await createDidAuthRequest();
109
-
110
- // First request should succeed
111
- const res1 = await client.post("/oauth/token", body);
112
- expect(res1.status).toBe(200);
113
-
114
- // Second request with the same body (same nonce) should fail
115
- const res2 = await client.post("/oauth/token", body);
116
- expect(res2.status).toBe(400);
117
- expect(res2.data.error).toBe("invalid_request");
118
- expect(res2.data.error_description).toContain("nonce");
119
- });
120
-
121
- it("returns 400 when required fields are missing", async () => {
122
- // Missing did
123
- const res1 = await client.post("/oauth/token", {
124
- grant_type: "did",
125
- signature: "abc",
126
- message: "{}",
127
- });
128
- expect(res1.status).toBe(400);
129
- expect(res1.data.error).toBe("invalid_request");
130
-
131
- // Missing signature
132
- const res2 = await client.post("/oauth/token", {
133
- grant_type: "did",
134
- did: "did:example:test",
135
- message: "{}",
136
- });
137
- expect(res2.status).toBe(400);
138
-
139
- // Missing message
140
- const res3 = await client.post("/oauth/token", {
141
- grant_type: "did",
142
- did: "did:example:test",
143
- signature: "abc",
144
- });
145
- expect(res3.status).toBe(400);
146
- });
147
-
148
- it("returns 400 when message is not valid JSON", async () => {
149
- const res = await client.post("/oauth/token", {
150
- grant_type: "did",
151
- did: "did:example:test",
152
- signature: "abc",
153
- message: "not-json",
154
- publicKey: "abc",
155
- });
156
- expect(res.status).toBe(400);
157
- expect(res.data.error_description).toContain("valid JSON");
158
- });
159
-
160
- it("returns 400 when message.did does not match top-level did", async () => {
161
- const res = await client.post("/oauth/token", {
162
- grant_type: "did",
163
- did: "did:example:one",
164
- signature: "abc",
165
- message: JSON.stringify({
166
- did: "did:example:other",
167
- timestamp: new Date().toISOString(),
168
- nonce: crypto.randomUUID(),
169
- }),
170
- publicKey: "abc",
171
- });
172
- expect(res.status).toBe(400);
173
- expect(res.data.error_description).toContain("must match");
174
- });
175
-
176
- it("returns 400 when publicKey is missing", async () => {
177
- const did = "did:example:test";
178
- const message = JSON.stringify({
179
- did,
180
- timestamp: new Date().toISOString(),
181
- nonce: crypto.randomUUID(),
182
- });
183
-
184
- const res = await client.post("/oauth/token", {
185
- grant_type: "did",
186
- did,
187
- signature: "abc",
188
- message,
189
- });
190
- expect(res.status).toBe(400);
191
- expect(res.data.error_description).toContain("publicKey");
192
- });
193
- });