@fonoster/authz 0.30.0 → 0.31.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.
@@ -1,4 +1,4 @@
1
- import { AddBillingMeterEventRequest, CheckMethodAuthorizedRequest, VoiceRequest } from "../types";
1
+ import { AddBillingMeterEventRequest, CheckMethodAuthorizedRequest, SessionDecision, VoiceRequest } from "../types";
2
2
  /**
3
3
  * AuthzClient class to interact with the AuthzServer via gRPC.
4
4
  */
@@ -15,6 +15,12 @@ export declare class AuthzClient {
15
15
  * @returns Promise resolving to a boolean indicating authorization.
16
16
  */
17
17
  checkSessionAuthorized(request: Partial<VoiceRequest>): Promise<boolean>;
18
+ /**
19
+ * Asks whether a session may start; a refusal names its reason (add-billing-service D7).
20
+ * @param request VoiceRequest containing session details.
21
+ * @param deadlineMs How long to wait for the answer before failing.
22
+ */
23
+ checkSession(request: Partial<VoiceRequest>, deadlineMs?: number): Promise<SessionDecision>;
18
24
  /**
19
25
  * Checks if a specific method is authorized.
20
26
  * @param request CheckMethodAuthorizedRequest containing accessKeyId and method.
@@ -85,6 +85,36 @@ class AuthzClient {
85
85
  });
86
86
  });
87
87
  }
88
+ /**
89
+ * Asks whether a session may start; a refusal names its reason (add-billing-service D7).
90
+ * @param request VoiceRequest containing session details.
91
+ * @param deadlineMs How long to wait for the answer before failing.
92
+ */
93
+ async checkSession(request, deadlineMs) {
94
+ const options = deadlineMs
95
+ ? { deadline: new Date(Date.now() + deadlineMs) }
96
+ : {};
97
+ return new Promise((resolve, reject) => {
98
+ this.client.checkSessionAuthorized(request, options, (error, response) => {
99
+ if (error) {
100
+ reject(new Error(`checkSession failed: ${error.message || error}`));
101
+ }
102
+ else if (response && typeof response.authorized === "boolean") {
103
+ resolve(response.authorized
104
+ ? { authorized: true }
105
+ : {
106
+ authorized: false,
107
+ ...(response.reason
108
+ ? { reason: response.reason }
109
+ : {})
110
+ });
111
+ }
112
+ else {
113
+ reject(new Error("checkSession failed: Invalid response format."));
114
+ }
115
+ });
116
+ });
117
+ }
88
118
  /**
89
119
  * Checks if a specific method is authorized.
90
120
  * @param request CheckMethodAuthorizedRequest containing accessKeyId and method.
@@ -25,6 +25,11 @@ import { AddBillingMeterEventRequest, CheckMethodAuthorizedRequest, VoiceRequest
25
25
  interface AuthzServiceClient extends grpc.Client {
26
26
  checkSessionAuthorized(request: Partial<VoiceRequest>, callback: grpc.requestCallback<{
27
27
  authorized: boolean;
28
+ reason?: string;
29
+ }>): void;
30
+ checkSessionAuthorized(request: Partial<VoiceRequest>, options: Partial<grpc.CallOptions>, callback: grpc.requestCallback<{
31
+ authorized: boolean;
32
+ reason?: string;
28
33
  }>): void;
29
34
  checkMethodAuthorized(request: CheckMethodAuthorizedRequest, callback: grpc.requestCallback<{
30
35
  authorized: boolean;
@@ -1,7 +1,11 @@
1
1
  import { AuthzHandler, ServerConfig } from "../types";
2
2
  declare class AuthzServer {
3
3
  config: ServerConfig;
4
+ private server?;
4
5
  constructor(config?: ServerConfig);
5
- listen(handler: AuthzHandler): Promise<void>;
6
+ /** Serves the handler; resolves with the bound port (undefined if binding failed). */
7
+ listen(handler: AuthzHandler): Promise<number | undefined>;
8
+ /** Stops serving at once. */
9
+ close(): void;
6
10
  }
7
11
  export { AuthzServer };
@@ -68,11 +68,13 @@ class AuthzServer {
68
68
  constructor(config = defaultServerConfig_1.defaultServerConfig) {
69
69
  this.config = (0, deepmerge_1.default)(defaultServerConfig_1.defaultServerConfig, config);
70
70
  }
71
+ /** Serves the handler; resolves with the bound port (undefined if binding failed). */
71
72
  async listen(handler) {
72
73
  try {
73
74
  const healthImpl = new grpc_health_check_1.HealthImplementation(common_1.statusMap);
74
75
  const credentials = await (0, common_1.getServerCredentials)({});
75
76
  const server = new grpc.Server();
77
+ this.server = server;
76
78
  server.addService(serviceDefinition_1.serviceDefinition, {
77
79
  checkSessionAuthorized: async (call, callback) => {
78
80
  // Not the request itself: it carries the caller's token (M11).
@@ -81,8 +83,10 @@ class AuthzServer {
81
83
  appRef: call.request?.appRef ?? call.request?.app_ref
82
84
  });
83
85
  try {
84
- const authorized = await handler.checkSessionAuthorized(call.request);
85
- callback(null, { authorized });
86
+ const decision = await handler.checkSessionAuthorized(call.request);
87
+ callback(null, typeof decision === "boolean"
88
+ ? { authorized: decision }
89
+ : { authorized: decision.authorized, reason: decision.reason });
86
90
  }
87
91
  catch (error) {
88
92
  logger.error("error in checkSessionAuthorized:", error);
@@ -133,18 +137,24 @@ class AuthzServer {
133
137
  });
134
138
  healthImpl.addToServer(server);
135
139
  const bindAddr = `${this.config.bind}:${this.config.port}`;
136
- server.bindAsync(bindAddr, credentials, async (err, port) => {
140
+ return await new Promise((resolve) => server.bindAsync(bindAddr, credentials, async (err, port) => {
137
141
  if (err) {
138
142
  logger.error("Failed to bind server:", err);
139
- return;
143
+ return resolve(undefined);
140
144
  }
141
145
  healthImpl.setStatus("", common_1.GRPC_SERVING_STATUS);
142
146
  logger.info(`Authz server started at ${this.config.bind}:${port}`);
143
- });
147
+ resolve(port);
148
+ }));
144
149
  }
145
150
  catch (err) {
146
151
  logger.error("Error starting AuthzServer:", err);
152
+ return undefined;
147
153
  }
148
154
  }
155
+ /** Stops serving at once. */
156
+ close() {
157
+ this.server?.forceShutdown();
158
+ }
149
159
  }
150
160
  exports.AuthzServer = AuthzServer;
@@ -1,6 +1,6 @@
1
- import { AddBillingMeterEventRequest, AuthzHandler, CheckMethodAuthorizedRequest, VoiceRequest } from "../types";
1
+ import { AddBillingMeterEventRequest, AuthzHandler, CheckMethodAuthorizedRequest, SessionDecision, VoiceRequest } from "../types";
2
2
  declare class DummyAuthzHandler implements AuthzHandler {
3
- checkSessionAuthorized(request: VoiceRequest): Promise<boolean>;
3
+ checkSessionAuthorized(request: VoiceRequest): Promise<SessionDecision>;
4
4
  checkMethodAuthorized(request: CheckMethodAuthorizedRequest): Promise<boolean>;
5
5
  addBillingMeterEvent(request: AddBillingMeterEventRequest): Promise<void>;
6
6
  }
@@ -24,7 +24,7 @@ const logger = (0, logger_1.getLogger)({ service: "authz", filePath: __filename
24
24
  class DummyAuthzHandler {
25
25
  async checkSessionAuthorized(request) {
26
26
  logger.verbose("checkSessionAuthorized called", request);
27
- return true;
27
+ return { authorized: true };
28
28
  }
29
29
  async checkMethodAuthorized(request) {
30
30
  logger.verbose("checkMethodAuthorized called", request);
package/dist/types.d.ts CHANGED
@@ -42,9 +42,17 @@ type AddBillingMeterEventRequest = {
42
42
  accessKeyId: string;
43
43
  payload: Record<string, unknown>;
44
44
  };
45
+ /** Why a session was refused (add-billing-service D7; trial_ended: a Trial past its 30 days). */
46
+ type SessionRefusal = "insufficient_balance" | "concurrency_limit" | "trial_ended";
47
+ /** A session admission answer: a refusal names its reason. */
48
+ type SessionDecision = {
49
+ authorized: boolean;
50
+ reason?: SessionRefusal;
51
+ };
45
52
  type AuthzHandler = {
46
- checkSessionAuthorized(request: VoiceRequest): Promise<boolean>;
53
+ /** A plain boolean is still accepted from handlers written before reasons existed. */
54
+ checkSessionAuthorized(request: VoiceRequest): Promise<boolean | SessionDecision>;
47
55
  checkMethodAuthorized(request: CheckMethodAuthorizedRequest): Promise<boolean>;
48
56
  addBillingMeterEvent(request: AddBillingMeterEventRequest): Promise<void>;
49
57
  };
50
- export { ServerConfig, AuthzHandler, VoiceRequest, CheckMethodAuthorizedRequest, AddBillingMeterEventRequest };
58
+ export { ServerConfig, AuthzHandler, SessionDecision, SessionRefusal, VoiceRequest, CheckMethodAuthorizedRequest, AddBillingMeterEventRequest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonoster/authz",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Authorization module for Fonoster",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
@@ -14,8 +14,7 @@
14
14
  "scripts": {
15
15
  "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
16
  "build": "tsc -b tsconfig.json",
17
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
- "start:dev": "nodemon --exec tsx --watch src src/server/server.ts"
17
+ "dev": "nodemon --exec tsx --watch src src/server/server.ts"
19
18
  },
20
19
  "files": [
21
20
  "dist"
@@ -24,9 +23,9 @@
24
23
  "access": "public"
25
24
  },
26
25
  "dependencies": {
27
- "@fonoster/common": "^0.30.0",
28
- "@fonoster/identity": "^0.30.0",
29
- "@fonoster/logger": "^0.30.0",
26
+ "@fonoster/common": "^0.31.0",
27
+ "@fonoster/identity": "^0.31.0",
28
+ "@fonoster/logger": "^0.31.0",
30
29
  "@grpc/grpc-js": "~1.14.6",
31
30
  "deepmerge": "^4.3.1",
32
31
  "grpc-health-check": "^2.0.2"
@@ -38,5 +37,5 @@
38
37
  "bugs": {
39
38
  "url": "https://github.com/fonoster/fonoster/issues"
40
39
  },
41
- "gitHead": "b59b06a3ef325abe3f188c0fb87cc22d50ed5471"
40
+ "gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e"
42
41
  }