akeyless-server-commons 1.0.204 → 1.0.206

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,358 @@
1
+ import { FieldValue, Timestamp } from "firebase-admin/firestore";
2
+ import { db } from "./firebase_helpers";
3
+ import { logger } from "../managers";
4
+ const POSTPONED_ACTIONS_COLLECTION = "nx-postponed-actions";
5
+ export var PostponedActionStatus;
6
+ (function (PostponedActionStatus) {
7
+ PostponedActionStatus["pending"] = "pending";
8
+ PostponedActionStatus["processing"] = "processing";
9
+ PostponedActionStatus["completed"] = "completed";
10
+ PostponedActionStatus["cancelled"] = "cancelled";
11
+ PostponedActionStatus["failed"] = "failed";
12
+ })(PostponedActionStatus || (PostponedActionStatus = {}));
13
+ const get_ref = (key) => {
14
+ if (!key.trim()) {
15
+ throw new Error("postponed action key must not be empty");
16
+ }
17
+ if (key.includes("/")) {
18
+ throw new Error("postponed action key must not contain '/'");
19
+ }
20
+ return db.collection(POSTPONED_ACTIONS_COLLECTION).doc(key);
21
+ };
22
+ export const resolve_postponed_action_time = (value, now = new Date()) => {
23
+ if (value instanceof Date || typeof value === "string") {
24
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
25
+ if (!Number.isFinite(date.getTime())) {
26
+ throw new Error("execute_at must be a valid date");
27
+ }
28
+ return date;
29
+ }
30
+ const { minutes = 0, hours = 0 } = value;
31
+ if (!Number.isFinite(minutes) || !Number.isFinite(hours) || minutes < 0 || hours < 0 || minutes + hours <= 0) {
32
+ throw new Error("relative execute_at must contain positive finite minutes or hours");
33
+ }
34
+ return new Date(now.getTime() + (minutes + hours * 60) * 60_000);
35
+ };
36
+ const validate_text = (value, field_name) => {
37
+ if (!value.trim()) {
38
+ throw new Error(`postponed action ${field_name} must not be empty`);
39
+ }
40
+ };
41
+ const validate_details = (details) => {
42
+ if (!details || Array.isArray(details) || typeof details !== "object") {
43
+ throw new Error("postponed action details must be a JSON object");
44
+ }
45
+ try {
46
+ JSON.stringify(details, (_key, value) => {
47
+ if (value === undefined || typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
48
+ throw new Error("unsupported JSON value");
49
+ }
50
+ if (typeof value === "number" && !Number.isFinite(value)) {
51
+ throw new Error("JSON numbers must be finite");
52
+ }
53
+ return value;
54
+ });
55
+ }
56
+ catch (error) {
57
+ throw new Error("postponed action details must be JSON serializable", { cause: error });
58
+ }
59
+ };
60
+ const timestamp_to_date = (value, field_name) => {
61
+ if (value instanceof Timestamp)
62
+ return value.toDate();
63
+ if (value instanceof Date)
64
+ return value;
65
+ throw new Error(`postponed action has invalid ${field_name}`);
66
+ };
67
+ const optional_timestamp_to_date = (value, field_name) => {
68
+ return value === undefined ? undefined : timestamp_to_date(value, field_name);
69
+ };
70
+ const to_postponed_action = (snapshot) => {
71
+ const value = snapshot.data();
72
+ if (!snapshot.exists || !value) {
73
+ throw new Error(`postponed action '${snapshot.id}' does not exist`);
74
+ }
75
+ return {
76
+ key: snapshot.id,
77
+ details: value.details,
78
+ action_type: value.action_type,
79
+ description: value.description,
80
+ execute_at: timestamp_to_date(value.execute_at, "execute_at"),
81
+ status: value.status,
82
+ attempt_count: typeof value.attempt_count === "number" ? value.attempt_count : 0,
83
+ created_at: timestamp_to_date(value.created_at, "created_at"),
84
+ updated_at: timestamp_to_date(value.updated_at, "updated_at"),
85
+ started_at: optional_timestamp_to_date(value.started_at, "started_at"),
86
+ completed_at: optional_timestamp_to_date(value.completed_at, "completed_at"),
87
+ cancelled_at: optional_timestamp_to_date(value.cancelled_at, "cancelled_at"),
88
+ failed_at: optional_timestamp_to_date(value.failed_at, "failed_at"),
89
+ error: typeof value.error === "string" ? value.error : undefined,
90
+ };
91
+ };
92
+ const assert_status = (key, actual, allowed) => {
93
+ if (!allowed.includes(actual)) {
94
+ throw new Error(`postponed action '${key}' has status '${String(actual)}'; expected ${allowed.join(" or ")}`);
95
+ }
96
+ };
97
+ const get_postponed_action_core = async (key) => {
98
+ const snapshot = await get_ref(key).get();
99
+ return snapshot.exists ? to_postponed_action(snapshot) : null;
100
+ };
101
+ const add_postponed_action_core = async (input) => {
102
+ validate_details(input.details);
103
+ validate_text(input.action_type, "action_type");
104
+ validate_text(input.description, "description");
105
+ const ref = get_ref(input.key);
106
+ const execute_at = resolve_postponed_action_time(input.execute_at);
107
+ const now = new Date();
108
+ return db.runTransaction(async (transaction) => {
109
+ const snapshot = await transaction.get(ref);
110
+ if (snapshot.exists) {
111
+ assert_status(input.key, snapshot.get("status"), [PostponedActionStatus.pending]);
112
+ transaction.update(ref, {
113
+ details: input.details,
114
+ action_type: input.action_type,
115
+ description: input.description,
116
+ execute_at,
117
+ updated_at: now,
118
+ });
119
+ return {
120
+ ...to_postponed_action(snapshot),
121
+ details: input.details,
122
+ action_type: input.action_type,
123
+ description: input.description,
124
+ execute_at,
125
+ updated_at: now,
126
+ };
127
+ }
128
+ const action = {
129
+ key: input.key,
130
+ details: input.details,
131
+ action_type: input.action_type,
132
+ description: input.description,
133
+ execute_at,
134
+ status: PostponedActionStatus.pending,
135
+ attempt_count: 0,
136
+ created_at: now,
137
+ updated_at: now,
138
+ };
139
+ transaction.create(ref, action);
140
+ return action;
141
+ });
142
+ };
143
+ const update_postponed_action_core = async (key, input) => {
144
+ if (input.details === undefined && input.action_type === undefined && input.description === undefined && input.execute_at === undefined) {
145
+ throw new Error("postponed action update must contain details, action_type, description or execute_at");
146
+ }
147
+ if (input.details !== undefined)
148
+ validate_details(input.details);
149
+ if (input.action_type !== undefined)
150
+ validate_text(input.action_type, "action_type");
151
+ if (input.description !== undefined)
152
+ validate_text(input.description, "description");
153
+ const ref = get_ref(key);
154
+ await db.runTransaction(async (transaction) => {
155
+ const snapshot = await transaction.get(ref);
156
+ if (!snapshot.exists)
157
+ throw new Error(`postponed action '${key}' does not exist`);
158
+ assert_status(key, snapshot.get("status"), [PostponedActionStatus.pending]);
159
+ transaction.update(ref, {
160
+ ...(input.details === undefined ? {} : { details: input.details }),
161
+ ...(input.action_type === undefined ? {} : { action_type: input.action_type }),
162
+ ...(input.description === undefined ? {} : { description: input.description }),
163
+ ...(input.execute_at === undefined ? {} : { execute_at: resolve_postponed_action_time(input.execute_at) }),
164
+ updated_at: new Date(),
165
+ });
166
+ });
167
+ };
168
+ const repostpone_postponed_action_core = async (key, execute_at) => {
169
+ const ref = get_ref(key);
170
+ await db.runTransaction(async (transaction) => {
171
+ const snapshot = await transaction.get(ref);
172
+ if (!snapshot.exists)
173
+ throw new Error(`postponed action '${key}' does not exist`);
174
+ assert_status(key, snapshot.get("status"), [
175
+ PostponedActionStatus.pending,
176
+ PostponedActionStatus.completed,
177
+ PostponedActionStatus.cancelled,
178
+ PostponedActionStatus.failed,
179
+ ]);
180
+ transaction.update(ref, {
181
+ execute_at: resolve_postponed_action_time(execute_at),
182
+ status: PostponedActionStatus.pending,
183
+ updated_at: new Date(),
184
+ started_at: FieldValue.delete(),
185
+ completed_at: FieldValue.delete(),
186
+ cancelled_at: FieldValue.delete(),
187
+ failed_at: FieldValue.delete(),
188
+ error: FieldValue.delete(),
189
+ });
190
+ });
191
+ };
192
+ const cancel_postponed_action_core = async (key) => {
193
+ const ref = get_ref(key);
194
+ await db.runTransaction(async (transaction) => {
195
+ const snapshot = await transaction.get(ref);
196
+ if (!snapshot.exists)
197
+ throw new Error(`postponed action '${key}' does not exist`);
198
+ assert_status(key, snapshot.get("status"), [PostponedActionStatus.pending]);
199
+ const now = new Date();
200
+ transaction.update(ref, { status: PostponedActionStatus.cancelled, cancelled_at: now, updated_at: now });
201
+ });
202
+ };
203
+ const complete_postponed_action_core = async (key) => {
204
+ const ref = get_ref(key);
205
+ await db.runTransaction(async (transaction) => {
206
+ const snapshot = await transaction.get(ref);
207
+ if (!snapshot.exists)
208
+ throw new Error(`postponed action '${key}' does not exist`);
209
+ assert_status(key, snapshot.get("status"), [PostponedActionStatus.processing]);
210
+ const now = new Date();
211
+ transaction.update(ref, { status: PostponedActionStatus.completed, completed_at: now, updated_at: now });
212
+ });
213
+ };
214
+ const fail_postponed_action_core = async (key, error) => {
215
+ const ref = get_ref(key);
216
+ await db.runTransaction(async (transaction) => {
217
+ const snapshot = await transaction.get(ref);
218
+ if (!snapshot.exists)
219
+ throw new Error(`postponed action '${key}' does not exist`);
220
+ assert_status(key, snapshot.get("status"), [PostponedActionStatus.processing]);
221
+ const now = new Date();
222
+ transaction.update(ref, {
223
+ status: PostponedActionStatus.failed,
224
+ failed_at: now,
225
+ updated_at: now,
226
+ error: error instanceof Error ? error.message : String(error),
227
+ });
228
+ });
229
+ logger.error(`[PostponedActions] action '${key}' failed`, error);
230
+ };
231
+ const delete_postponed_action_core = async (key) => {
232
+ await get_ref(key).delete();
233
+ };
234
+ const claim_postponed_action_core = async (key, action_type, now = new Date()) => {
235
+ validate_text(action_type, "action_type");
236
+ const ref = get_ref(key);
237
+ return db.runTransaction(async (transaction) => {
238
+ const snapshot = await transaction.get(ref);
239
+ if (!snapshot.exists || snapshot.get("status") !== PostponedActionStatus.pending || snapshot.get("action_type") !== action_type) {
240
+ return null;
241
+ }
242
+ const action = to_postponed_action(snapshot);
243
+ if (action.execute_at.getTime() > now.getTime())
244
+ return null;
245
+ const claimed = {
246
+ ...action,
247
+ status: PostponedActionStatus.processing,
248
+ attempt_count: action.attempt_count + 1,
249
+ started_at: now,
250
+ updated_at: now,
251
+ };
252
+ transaction.update(ref, {
253
+ status: claimed.status,
254
+ attempt_count: claimed.attempt_count,
255
+ started_at: now,
256
+ updated_at: now,
257
+ });
258
+ return claimed;
259
+ });
260
+ };
261
+ const claim_due_postponed_actions_core = async (action_type, limit = 100, now = new Date()) => {
262
+ validate_text(action_type, "action_type");
263
+ if (!Number.isInteger(limit) || limit <= 0) {
264
+ throw new Error("limit must be a positive integer");
265
+ }
266
+ const snapshot = await db
267
+ .collection(POSTPONED_ACTIONS_COLLECTION)
268
+ .where("action_type", "==", action_type)
269
+ .where("status", "==", PostponedActionStatus.pending)
270
+ .where("execute_at", "<=", now)
271
+ .orderBy("execute_at")
272
+ .limit(limit)
273
+ .get();
274
+ const claimed = await Promise.all(snapshot.docs.map((document) => claim_postponed_action_core(document.id, action_type, now)));
275
+ return claimed.filter((action) => action !== null);
276
+ };
277
+ const with_error_logging = (operation, action, get_key) => {
278
+ return async (...args) => {
279
+ try {
280
+ return await action(...args);
281
+ }
282
+ catch (error) {
283
+ const key = get_key?.(...args);
284
+ logger.error(`[PostponedActions] ${operation} failed${key ? ` for '${key}'` : ""}`, error);
285
+ throw error;
286
+ }
287
+ };
288
+ };
289
+ export const get_postponed_action = with_error_logging("get", get_postponed_action_core, (key) => key);
290
+ export const add_postponed_action = with_error_logging("add", add_postponed_action_core, (input) => input.key);
291
+ export const update_postponed_action = with_error_logging("update", update_postponed_action_core, (key) => key);
292
+ export const repostpone_postponed_action = with_error_logging("repostpone", repostpone_postponed_action_core, (key) => key);
293
+ export const cancel_postponed_action = with_error_logging("cancel", cancel_postponed_action_core, (key) => key);
294
+ export const complete_postponed_action = with_error_logging("complete", complete_postponed_action_core, (key) => key);
295
+ export const fail_postponed_action = with_error_logging("mark as failed", fail_postponed_action_core, (key) => key);
296
+ export const delete_postponed_action = with_error_logging("delete", delete_postponed_action_core, (key) => key);
297
+ export const claim_postponed_action = with_error_logging("claim", claim_postponed_action_core, (key) => key);
298
+ export const claim_due_postponed_actions = with_error_logging("claim due", claim_due_postponed_actions_core);
299
+ /**
300
+ * @example Add an action for a specific date and time.
301
+ * ```ts
302
+ * await postponed_actions.add({
303
+ * key: "subscription:123:expire",
304
+ * action_type: "subscription_expiration",
305
+ * description: "Expire subscription 123",
306
+ * details: { subscription_id: "123", notify_customer: true },
307
+ * execute_at: new Date("2026-08-01T12:00:00Z"),
308
+ * });
309
+ * ```
310
+ *
311
+ * @example Add an action relative to the current time. Adding the same key
312
+ * updates the pending action.
313
+ * ```ts
314
+ * await postponed_actions.add({
315
+ * key: "invoice:456:reminder",
316
+ * action_type: "invoice_reminder",
317
+ * description: "Send invoice 456 reminder",
318
+ * details: { invoice_id: "456" },
319
+ * execute_at: { hours: 2, minutes: 30 },
320
+ * });
321
+ * ```
322
+ *
323
+ * @example Update, repostpone, or cancel a pending action.
324
+ * ```ts
325
+ * await postponed_actions.update("invoice:456:reminder", {
326
+ * description: "Send the final invoice reminder",
327
+ * execute_at: { minutes: 15 },
328
+ * });
329
+ * await postponed_actions.repostpone("invoice:456:reminder", { hours: 1 });
330
+ * await postponed_actions.cancel("invoice:456:reminder");
331
+ * ```
332
+ *
333
+ * @example Claim and execute due actions. Claiming is atomic, so concurrent
334
+ * workers cannot claim the same action.
335
+ * ```ts
336
+ * const actions = await postponed_actions.claim_due("invoice_reminder", 25);
337
+ * for (const action of actions) {
338
+ * try {
339
+ * await execute_action(action.details);
340
+ * await postponed_actions.complete(action.key);
341
+ * } catch (error) {
342
+ * await postponed_actions.fail(action.key, error);
343
+ * }
344
+ * }
345
+ * ```
346
+ */
347
+ export const postponed_actions = {
348
+ get: get_postponed_action,
349
+ add: add_postponed_action,
350
+ update: update_postponed_action,
351
+ repostpone: repostpone_postponed_action,
352
+ cancel: cancel_postponed_action,
353
+ complete: complete_postponed_action,
354
+ fail: fail_postponed_action,
355
+ delete: delete_postponed_action,
356
+ claim: claim_postponed_action,
357
+ claim_due: claim_due_postponed_actions,
358
+ };
@@ -111,6 +111,9 @@ export class RabbitManager {
111
111
  return event;
112
112
  }
113
113
  async open_connection() {
114
+ if (!(process.env.RABBITMQ_URL ?? "")) {
115
+ return;
116
+ }
114
117
  const connection_url = new URL(process.env.RABBITMQ_URL);
115
118
  if (!connection_url.searchParams.has("heartbeat")) {
116
119
  connection_url.searchParams.set("heartbeat", "30");
@@ -7,6 +7,7 @@ export * from "./notification_helpers";
7
7
  export * from "./email_helpers";
8
8
  export * from "./phone_number_helpers";
9
9
  export * from "./tasks_helpers";
10
+ export * from "./postponed_actions";
10
11
  export * from "./boards_helpers";
11
12
  export * from "./redis";
12
13
  export * from "./location_helpers";
@@ -0,0 +1,113 @@
1
+ import { TObject } from "akeyless-types-commons";
2
+ export declare enum PostponedActionStatus {
3
+ pending = "pending",
4
+ processing = "processing",
5
+ completed = "completed",
6
+ cancelled = "cancelled",
7
+ failed = "failed"
8
+ }
9
+ export type PostponedActionTime = Date | string | {
10
+ minutes?: number;
11
+ hours?: number;
12
+ };
13
+ export type JsonPayload = TObject<any>;
14
+ export interface PostponedAction {
15
+ key: string;
16
+ details: JsonPayload;
17
+ action_type: string;
18
+ description: string;
19
+ execute_at: Date;
20
+ status: PostponedActionStatus;
21
+ attempt_count: number;
22
+ created_at: Date;
23
+ updated_at: Date;
24
+ started_at?: Date;
25
+ completed_at?: Date;
26
+ cancelled_at?: Date;
27
+ failed_at?: Date;
28
+ error?: string;
29
+ }
30
+ export interface AddPostponedAction {
31
+ key: string;
32
+ details: JsonPayload;
33
+ action_type: string;
34
+ description: string;
35
+ execute_at: PostponedActionTime;
36
+ }
37
+ export interface UpdatePostponedAction {
38
+ details?: JsonPayload;
39
+ action_type?: string;
40
+ description?: string;
41
+ execute_at?: PostponedActionTime;
42
+ }
43
+ export declare const resolve_postponed_action_time: (value: PostponedActionTime, now?: Date) => Date;
44
+ export declare const get_postponed_action: (key: string) => Promise<PostponedAction | null>;
45
+ export declare const add_postponed_action: (input: AddPostponedAction) => Promise<PostponedAction>;
46
+ export declare const update_postponed_action: (key: string, input: UpdatePostponedAction) => Promise<void>;
47
+ export declare const repostpone_postponed_action: (key: string, execute_at: PostponedActionTime) => Promise<void>;
48
+ export declare const cancel_postponed_action: (key: string) => Promise<void>;
49
+ export declare const complete_postponed_action: (key: string) => Promise<void>;
50
+ export declare const fail_postponed_action: (key: string, error: unknown) => Promise<void>;
51
+ export declare const delete_postponed_action: (key: string) => Promise<void>;
52
+ export declare const claim_postponed_action: (key: string, action_type: string, now?: Date | undefined) => Promise<PostponedAction | null>;
53
+ export declare const claim_due_postponed_actions: (action_type: string, limit?: number | undefined, now?: Date | undefined) => Promise<PostponedAction[]>;
54
+ /**
55
+ * @example Add an action for a specific date and time.
56
+ * ```ts
57
+ * await postponed_actions.add({
58
+ * key: "subscription:123:expire",
59
+ * action_type: "subscription_expiration",
60
+ * description: "Expire subscription 123",
61
+ * details: { subscription_id: "123", notify_customer: true },
62
+ * execute_at: new Date("2026-08-01T12:00:00Z"),
63
+ * });
64
+ * ```
65
+ *
66
+ * @example Add an action relative to the current time. Adding the same key
67
+ * updates the pending action.
68
+ * ```ts
69
+ * await postponed_actions.add({
70
+ * key: "invoice:456:reminder",
71
+ * action_type: "invoice_reminder",
72
+ * description: "Send invoice 456 reminder",
73
+ * details: { invoice_id: "456" },
74
+ * execute_at: { hours: 2, minutes: 30 },
75
+ * });
76
+ * ```
77
+ *
78
+ * @example Update, repostpone, or cancel a pending action.
79
+ * ```ts
80
+ * await postponed_actions.update("invoice:456:reminder", {
81
+ * description: "Send the final invoice reminder",
82
+ * execute_at: { minutes: 15 },
83
+ * });
84
+ * await postponed_actions.repostpone("invoice:456:reminder", { hours: 1 });
85
+ * await postponed_actions.cancel("invoice:456:reminder");
86
+ * ```
87
+ *
88
+ * @example Claim and execute due actions. Claiming is atomic, so concurrent
89
+ * workers cannot claim the same action.
90
+ * ```ts
91
+ * const actions = await postponed_actions.claim_due("invoice_reminder", 25);
92
+ * for (const action of actions) {
93
+ * try {
94
+ * await execute_action(action.details);
95
+ * await postponed_actions.complete(action.key);
96
+ * } catch (error) {
97
+ * await postponed_actions.fail(action.key, error);
98
+ * }
99
+ * }
100
+ * ```
101
+ */
102
+ export declare const postponed_actions: {
103
+ get: (key: string) => Promise<PostponedAction | null>;
104
+ add: (input: AddPostponedAction) => Promise<PostponedAction>;
105
+ update: (key: string, input: UpdatePostponedAction) => Promise<void>;
106
+ repostpone: (key: string, execute_at: PostponedActionTime) => Promise<void>;
107
+ cancel: (key: string) => Promise<void>;
108
+ complete: (key: string) => Promise<void>;
109
+ fail: (key: string, error: unknown) => Promise<void>;
110
+ delete: (key: string) => Promise<void>;
111
+ claim: (key: string, action_type: string, now?: Date | undefined) => Promise<PostponedAction | null>;
112
+ claim_due: (action_type: string, limit?: number | undefined, now?: Date | undefined) => Promise<PostponedAction[]>;
113
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akeyless-server-commons",
3
- "version": "1.0.204",
3
+ "version": "1.0.206",
4
4
  "scripts": {
5
5
  "build:cjs": "tsc --project tsconfig.cjs.json",
6
6
  "build:esm": "tsc --project tsconfig.esm.json",