@reboot-dev/reboot 0.24.0 → 0.25.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/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { errors_pb, IdempotencyOptions, protobuf_es, ScheduleOptions } from "@reboot-dev/reboot-api";
2
+ import express from "express";
2
3
  import * as reboot_native from "./reboot_native.cjs";
3
4
  export { reboot_native };
4
5
  export * from "./utils/index.js";
5
- type ApplicationConfig = {
6
+ type ApplicationRevision = {
6
7
  applicationId: string;
7
8
  };
8
9
  export declare class Reboot {
@@ -16,10 +17,9 @@ export declare class Reboot {
16
17
  }): ExternalContext;
17
18
  start(): Promise<void>;
18
19
  stop(): Promise<void>;
19
- up(servicers: any, options?: {
20
- tokenVerifier?: TokenVerifier;
20
+ up(application: Application, options?: {
21
21
  localEnvoy?: boolean;
22
- }): Promise<ApplicationConfig>;
22
+ }): Promise<ApplicationRevision>;
23
23
  down(): Promise<void>;
24
24
  url(): string;
25
25
  }
@@ -48,9 +48,10 @@ export declare class Context {
48
48
  static fromNativeExternal(external: any, kind: string, aborted: Promise<void>): ReaderContext | WriterContext | TransactionContext | WorkflowContext;
49
49
  get __external(): any;
50
50
  get auth(): Auth | null;
51
- get stateId(): any;
52
- get iteration(): any;
53
- get cookie(): any;
51
+ get stateId(): string;
52
+ get iteration(): number;
53
+ get cookie(): string;
54
+ get appInternal(): boolean;
54
55
  generateIdempotentStateId(stateType: string, serviceName: string, method: string, idempotency: IdempotencyOptions): Promise<any>;
55
56
  }
56
57
  export declare class ReaderContext extends Context {
@@ -145,12 +146,38 @@ export declare abstract class Authorizer<StateType, RequestTypes> {
145
146
  abstract authorize(methodName: string, context: ReaderContext, state?: StateType, request?: RequestTypes): Promise<AuthorizerDecision>;
146
147
  _authorize?: (methodName: string, context: ReaderContext, bytesState?: Uint8Array, bytesRequest?: Uint8Array) => Promise<Uint8Array>;
147
148
  }
148
- /**
149
- * An authorizer that allows all requests, as long as the caller is authenticated.
150
- */
151
- export declare class AllowAllIfAuthenticated extends Authorizer<protobuf_es.Message, protobuf_es.Message> {
152
- authorize(methodName: string, context: ReaderContext, state?: protobuf_es.Message, request?: protobuf_es.Message): Promise<AuthorizerDecision>;
149
+ export type AuthorizerCallable<StateType, RequestType> = (args: {
150
+ context: ReaderContext;
151
+ state?: StateType;
152
+ request?: RequestType;
153
+ }) => Promise<AuthorizerDecision> | AuthorizerDecision;
154
+ export declare abstract class AuthorizerRule<StateType, RequestType> {
155
+ abstract execute(args: {
156
+ context: ReaderContext;
157
+ state?: StateType;
158
+ request?: RequestType;
159
+ }): Promise<AuthorizerDecision>;
153
160
  }
161
+ export declare function deny(): AuthorizerRule<protobuf_es.Message, protobuf_es.Message>;
162
+ export declare function allow(): AuthorizerRule<protobuf_es.Message, protobuf_es.Message>;
163
+ export declare function allowIf<StateType, RequestType>(args: {
164
+ all: AuthorizerCallable<StateType, RequestType>[];
165
+ any?: never;
166
+ }): AuthorizerRule<StateType, RequestType>;
167
+ export declare function allowIf<StateType, RequestType>(args: {
168
+ all?: never;
169
+ any: AuthorizerCallable<StateType, RequestType>[];
170
+ }): AuthorizerRule<StateType, RequestType>;
171
+ export declare function hasVerifiedToken({ context, }: {
172
+ context: ReaderContext;
173
+ state?: protobuf_es.Message;
174
+ request?: protobuf_es.Message;
175
+ }): errors_pb.Unauthenticated | errors_pb.Ok;
176
+ export declare function isAppInternal({ context, }: {
177
+ context: ReaderContext;
178
+ state?: protobuf_es.Message;
179
+ request?: protobuf_es.Message;
180
+ }): errors_pb.Unauthenticated | errors_pb.Ok;
154
181
  export declare class Application {
155
182
  #private;
156
183
  constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }: {
@@ -159,7 +186,30 @@ export declare class Application {
159
186
  initializeBearerToken?: string;
160
187
  tokenVerifier?: TokenVerifier;
161
188
  });
189
+ get servicers(): ServicerFactory[];
190
+ get tokenVerifier(): TokenVerifier;
191
+ get http(): Application.Http;
162
192
  run(): Promise<any>;
193
+ get __external(): any;
194
+ }
195
+ export declare namespace Application {
196
+ namespace Http {
197
+ type Path = string | RegExp | (string | RegExp)[];
198
+ type ReqResHandler = (context: ExternalContext, req: express.Request, res: express.Response) => any;
199
+ type ReqResNextHandler = (context: ExternalContext, req: express.Request, res: express.Response, next: express.NextFunction) => any;
200
+ type ErrReqResNextHandler = (context: ExternalContext, err: any, req: express.Request, res: express.Response, next: express.NextFunction) => any;
201
+ type Handler = ReqResHandler | ReqResNextHandler | ErrReqResNextHandler;
202
+ }
203
+ class Http {
204
+ #private;
205
+ constructor(express: express.Application, createExternalContext: (args: {
206
+ name: string;
207
+ bearerToken?: string;
208
+ }) => Promise<ExternalContext>);
209
+ private add;
210
+ get(path: Http.Path, ...handlers: Http.Handler[]): void;
211
+ post(path: Http.Path, ...handlers: Http.Handler[]): void;
212
+ }
163
213
  }
164
214
  /**
165
215
  * @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
package/index.js CHANGED
@@ -9,12 +9,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_external;
12
+ var _Reboot_external, _ExternalContext_external, _a, _Context_external, _Context_isInternalConstructing, _ReaderContext_kind, _WriterContext_kind, _TransactionContext_kind, _WorkflowContext_kind, _Application_servicers, _Application_tokenVerifier, _Application_express, _Application_http, _Application_servers, _Application_createExternalContext, _Application_external;
13
13
  import { auth_pb, errors_pb, protobuf_es, } from "@reboot-dev/reboot-api";
14
14
  import { strict as assert } from "assert";
15
+ import express from "express";
16
+ import { AsyncLocalStorage } from "node:async_hooks";
15
17
  import { fork } from "node:child_process";
16
18
  import { test } from "node:test";
17
- import { AsyncLocalStorage } from "node:async_hooks";
18
19
  import * as reboot_native from "./reboot_native.cjs";
19
20
  import { ensurePythonVenv } from "./venv.js";
20
21
  export { reboot_native };
@@ -84,10 +85,10 @@ export class Reboot {
84
85
  startedInstances.splice(indexOfTimestamp, 1);
85
86
  }
86
87
  }
87
- async up(servicers, options) {
88
+ async up(application, options) {
88
89
  // TODO(benh): determine module and file name so that we can
89
90
  // namespace if we have more than one implementation of servicers.
90
- return await reboot_native.Reboot_up(__classPrivateFieldGet(this, _Reboot_external, "f"), servicers, options?.tokenVerifier, options?.localEnvoy);
91
+ return await reboot_native.Reboot_up(__classPrivateFieldGet(this, _Reboot_external, "f"), application.__external, options?.localEnvoy);
91
92
  }
92
93
  async down() {
93
94
  await reboot_native.Reboot_down(__classPrivateFieldGet(this, _Reboot_external, "f"));
@@ -178,6 +179,9 @@ export class Context {
178
179
  get cookie() {
179
180
  return reboot_native.Context_cookie(__classPrivateFieldGet(this, _Context_external, "f"));
180
181
  }
182
+ get appInternal() {
183
+ return reboot_native.Context_appInternal(__classPrivateFieldGet(this, _Context_external, "f"));
184
+ }
181
185
  async generateIdempotentStateId(stateType, serviceName, method, idempotency) {
182
186
  return reboot_native.Context_generateIdempotentStateId(__classPrivateFieldGet(this, _Context_external, "f"), stateType, serviceName, method, idempotency);
183
187
  }
@@ -324,21 +328,173 @@ export class TokenVerifier {
324
328
  */
325
329
  export class Authorizer {
326
330
  }
327
- /**
328
- * An authorizer that allows all requests, as long as the caller is authenticated.
329
- */
330
- export class AllowAllIfAuthenticated extends Authorizer {
331
- async authorize(methodName, context, state, request) {
332
- if (!context.auth) {
333
- return new errors_pb.Unauthenticated();
331
+ export class AuthorizerRule {
332
+ }
333
+ export function deny() {
334
+ return new (class extends AuthorizerRule {
335
+ async execute(args) {
336
+ return new errors_pb.PermissionDenied();
337
+ }
338
+ })();
339
+ }
340
+ export function allow() {
341
+ return new (class extends AuthorizerRule {
342
+ async execute(args) {
343
+ return new errors_pb.Ok();
344
+ }
345
+ })();
346
+ }
347
+ export function allowIf(args) {
348
+ const all = args.all;
349
+ const any = args.any;
350
+ if ((all === undefined && any === undefined) ||
351
+ (all !== undefined && any !== undefined)) {
352
+ throw new Error("Exactly one of `all` or `any` must be passed");
353
+ }
354
+ const callables = all ?? any;
355
+ return new (class extends AuthorizerRule {
356
+ async execute({ context, state, request }) {
357
+ // NOTE: we invoke each authorizer callable **one at a time**
358
+ // instead of concurrently so that:
359
+ //
360
+ // (1) We support dependency semantics for `all`, i.e.,
361
+ // callable's later can assume earlier callables did not
362
+ // return `Unauthenticated` or `PermissionDenied`.
363
+ //
364
+ // (2) We support short-circuiting`, i.e., cheaper authorizer
365
+ // callables can be listed first so more expensive ones
366
+ // aren't executed unless necessary.
367
+ //
368
+ // PLEASE KEEP SEMANTICS IN SYNC WITH PYTHON.
369
+ // Remember if we had any `PermissionDenied` for `any` so that
370
+ // we return that instead of `Unauthenticated`.
371
+ let denied = false;
372
+ for (const callable of callables) {
373
+ const decision = await callable({ context, state, request });
374
+ if (decision instanceof errors_pb.Ok) {
375
+ if (all !== undefined) {
376
+ // All callables must return `Ok`, keep checking.
377
+ continue;
378
+ }
379
+ else {
380
+ // Only needed one `Ok` and we got it, short-circuit.
381
+ return decision;
382
+ }
383
+ }
384
+ else if (decision instanceof errors_pb.Unauthenticated) {
385
+ if (all !== undefined) {
386
+ // All callables must return `Ok`, short-circuit.
387
+ return decision;
388
+ }
389
+ else {
390
+ // Just need one `Ok`, keep checking.
391
+ continue;
392
+ }
393
+ }
394
+ else if (decision instanceof errors_pb.PermissionDenied) {
395
+ if (all !== undefined) {
396
+ // All callables must return `Ok`, short-circuit.
397
+ return decision;
398
+ }
399
+ else {
400
+ // Remember that we got at least one `PermissionDenied` so
401
+ // we can return it later.
402
+ denied = true;
403
+ // Only need one `Ok`, keep checking.
404
+ continue;
405
+ }
406
+ }
407
+ }
408
+ // If this was `all`, then they must have all been `Ok`!
409
+ if (all !== undefined) {
410
+ // TODO: assert !denied
411
+ return new errors_pb.Ok();
412
+ }
413
+ // Must be `any`, check if we got a `PermissionDenied` otherwise
414
+ // return `Unauthenticated`.
415
+ if (denied) {
416
+ return new errors_pb.PermissionDenied();
417
+ }
418
+ else {
419
+ return new errors_pb.Unauthenticated();
420
+ }
334
421
  }
422
+ })();
423
+ }
424
+ export function hasVerifiedToken({ context, }) {
425
+ if (!context.auth) {
426
+ return new errors_pb.Unauthenticated();
427
+ }
428
+ return new errors_pb.Ok();
429
+ }
430
+ export function isAppInternal({ context, }) {
431
+ if (context.appInternal) {
335
432
  return new errors_pb.Ok();
336
433
  }
434
+ return new errors_pb.Unauthenticated();
337
435
  }
338
436
  export class Application {
339
437
  constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }) {
438
+ _Application_servicers.set(this, void 0);
439
+ _Application_tokenVerifier.set(this, void 0);
440
+ _Application_express.set(this, void 0);
441
+ _Application_http.set(this, void 0);
442
+ _Application_servers.set(this, void 0);
443
+ _Application_createExternalContext.set(this, void 0);
340
444
  _Application_external.set(this, void 0);
341
- __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, async (context) => {
445
+ __classPrivateFieldSet(this, _Application_servicers, servicers, "f");
446
+ __classPrivateFieldSet(this, _Application_tokenVerifier, tokenVerifier, "f");
447
+ __classPrivateFieldSet(this, _Application_express, express(), "f");
448
+ // We assume that our users will want these middleware.
449
+ // TODO: expose `.use()` to allow users to add their own middleware.
450
+ __classPrivateFieldGet(this, _Application_express, "f").use(express.json());
451
+ __classPrivateFieldGet(this, _Application_express, "f").use(express.urlencoded({ extended: true }));
452
+ __classPrivateFieldSet(this, _Application_http, new Application.Http(__classPrivateFieldGet(this, _Application_express, "f"), async (args) => {
453
+ return await __classPrivateFieldGet(this, _Application_createExternalContext, "f").call(this, args);
454
+ }), "f");
455
+ __classPrivateFieldSet(this, _Application_servers, new Map(), "f");
456
+ __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, {
457
+ start: async (consensusId, port, createExternalContext) => {
458
+ // Store `createExternalContext` function before listening
459
+ // so we don't attempt to serve any traffic and try and use
460
+ // an `undefined` function.
461
+ __classPrivateFieldSet(this, _Application_createExternalContext, createExternalContext, "f");
462
+ let server;
463
+ [server, port] = await new Promise((resolve, reject) => {
464
+ const server = __classPrivateFieldGet(this, _Application_express, "f").listen(port ?? 0, (error) => {
465
+ if (error) {
466
+ reject(error);
467
+ }
468
+ else {
469
+ // We requested a port so we should have an
470
+ // `AddressInfo` not a string representing a file
471
+ // descriptor path for a pipe or socket, etc.
472
+ const address = server.address();
473
+ assert(typeof address !== "string");
474
+ resolve([server, address.port]);
475
+ }
476
+ });
477
+ });
478
+ __classPrivateFieldGet(this, _Application_servers, "f").set(consensusId, server);
479
+ return port;
480
+ },
481
+ stop: async (consensusId) => {
482
+ const server = __classPrivateFieldGet(this, _Application_servers, "f").get(consensusId);
483
+ if (server !== undefined) {
484
+ await new Promise((resolve, reject) => {
485
+ server.close((err) => {
486
+ if (err) {
487
+ reject(err);
488
+ }
489
+ else {
490
+ resolve();
491
+ }
492
+ });
493
+ });
494
+ __classPrivateFieldGet(this, _Application_servers, "f").delete(consensusId);
495
+ }
496
+ },
497
+ }, async (context) => {
342
498
  if (initialize !== undefined) {
343
499
  try {
344
500
  await initialize(context);
@@ -354,11 +510,98 @@ export class Application {
354
510
  }
355
511
  }, initializeBearerToken, tokenVerifier), "f");
356
512
  }
513
+ get servicers() {
514
+ return __classPrivateFieldGet(this, _Application_servicers, "f");
515
+ }
516
+ get tokenVerifier() {
517
+ return __classPrivateFieldGet(this, _Application_tokenVerifier, "f");
518
+ }
519
+ get http() {
520
+ return __classPrivateFieldGet(this, _Application_http, "f");
521
+ }
357
522
  async run() {
358
523
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
359
524
  }
525
+ get __external() {
526
+ return __classPrivateFieldGet(this, _Application_external, "f");
527
+ }
360
528
  }
361
- _Application_external = new WeakMap();
529
+ _Application_servicers = new WeakMap(), _Application_tokenVerifier = new WeakMap(), _Application_express = new WeakMap(), _Application_http = new WeakMap(), _Application_servers = new WeakMap(), _Application_createExternalContext = new WeakMap(), _Application_external = new WeakMap();
530
+ (function (Application) {
531
+ var _Http_express, _Http_createExternalContext;
532
+ class Http {
533
+ constructor(express, createExternalContext) {
534
+ _Http_express.set(this, void 0);
535
+ _Http_createExternalContext.set(this, void 0);
536
+ __classPrivateFieldSet(this, _Http_express, express, "f");
537
+ __classPrivateFieldSet(this, _Http_createExternalContext, createExternalContext, "f");
538
+ }
539
+ add(method, path, handlers) {
540
+ const methods = {
541
+ GET: (path, ...handlers) => __classPrivateFieldGet(this, _Http_express, "f").get(path, ...handlers),
542
+ POST: (path, ...handlers) => __classPrivateFieldGet(this, _Http_express, "f").post(path, ...handlers),
543
+ };
544
+ const createExternalContext = (name, authorization) => {
545
+ let bearerToken = undefined;
546
+ if (authorization !== undefined) {
547
+ const parts = authorization.split(" ");
548
+ if (parts.length === 2 && parts[0].toLowerCase() === "bearer") {
549
+ bearerToken = parts[1];
550
+ }
551
+ }
552
+ return __classPrivateFieldGet(this, _Http_createExternalContext, "f").call(this, {
553
+ name,
554
+ bearerToken,
555
+ });
556
+ };
557
+ methods[method](path, ...handlers.map((handler) => {
558
+ // Express allows for three different kinds of handlers, a
559
+ // "normal" handler that takes two args, `req` and `res`, a
560
+ // "middlware" handler that takes three args, `req, `res`,
561
+ // and `next`, and an "error" handler that takes four args
562
+ // `err`, `req`, `res`, and `next`. To the best of our
563
+ // understanding they determine what arguments to pass to
564
+ // handlers based on how many arguments the handler takes,
565
+ // and so that is exactly what we do below as well, except
566
+ // accounting for an extra first arg for the
567
+ // `ExternalContext`.
568
+ switch (handler.length) {
569
+ // (context, req, res) => ...
570
+ case 3:
571
+ return async (req, res) => {
572
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
573
+ // @ts-ignore
574
+ handler(context, req, res);
575
+ };
576
+ // (context, req, res, next) => ...
577
+ case 4:
578
+ return async (req, res, next) => {
579
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
580
+ // @ts-ignore
581
+ handler(context, req, res, next);
582
+ };
583
+ // (context, err, req, res, next) => ...
584
+ case 5:
585
+ return async (err, req, res, next) => {
586
+ const context = await createExternalContext(`HTTP ${method} '${req.path}'`, req.header("Authorization"));
587
+ handler(context, err, req, res, next);
588
+ };
589
+ default:
590
+ throw new Error(`HTTP ${method} handler for '${path}' has unexpected ` +
591
+ `number of arguments`);
592
+ }
593
+ }));
594
+ }
595
+ get(path, ...handlers) {
596
+ this.add("GET", path, handlers);
597
+ }
598
+ post(path, ...handlers) {
599
+ this.add("POST", path, handlers);
600
+ }
601
+ }
602
+ _Http_express = new WeakMap(), _Http_createExternalContext = new WeakMap();
603
+ Application.Http = Http;
604
+ })(Application || (Application = {}));
362
605
  /**
363
606
  * @deprecated testWithReboot is deprecated in favor of the manual start of Reboot in the test setup.
364
607
  */
package/package.json CHANGED
@@ -3,18 +3,19 @@
3
3
  "@bufbuild/protobuf": "1.3.2",
4
4
  "@bufbuild/protoplugin": "1.3.2",
5
5
  "@bufbuild/protoc-gen-es": "1.3.2",
6
- "@reboot-dev/reboot-api": "0.24.0",
6
+ "@reboot-dev/reboot-api": "0.25.0",
7
7
  "chalk": "^4.1.2",
8
8
  "node-addon-api": "^7.0.0",
9
9
  "node-gyp": ">=10.2.0",
10
10
  "uuid": "^9.0.1",
11
11
  "which-pm-runs": "^1.1.0",
12
12
  "extensionless": "^1.9.9",
13
- "esbuild": "^0.24.0"
13
+ "esbuild": "^0.25.0",
14
+ "express": "^5.1.0"
14
15
  },
15
16
  "type": "module",
16
17
  "name": "@reboot-dev/reboot",
17
- "version": "0.24.0",
18
+ "version": "0.25.0",
18
19
  "description": "npm package for Reboot",
19
20
  "scripts": {
20
21
  "postinstall": "rbt || exit 0",
@@ -24,7 +25,8 @@
24
25
  "devDependencies": {
25
26
  "typescript": ">=4.9.5",
26
27
  "@types/node": "20.11.5",
27
- "@types/uuid": "^9.0.4"
28
+ "@types/uuid": "^9.0.4",
29
+ "@types/express": "^5.0.1"
28
30
  },
29
31
  "bin": {
30
32
  "rbt": "./rbt.js",
package/rbt.js CHANGED
@@ -27,13 +27,16 @@ function addExtensionlessToNodeOptions() {
27
27
  // If we have one of those two versions then we can pre import
28
28
  // `extensionless/register` to use the `module.register()` function,
29
29
  // otherwise we need to fall back to `--experimental-loader`.
30
- if (major > 20 ||
30
+ if (major >= 21 ||
31
31
  (major == 20 && minor >= 6) ||
32
32
  (major == 18 && minor >= 19)) {
33
33
  process.env.NODE_OPTIONS += "--import=extensionless/register";
34
34
  }
35
35
  else {
36
- process.env.NODE_OPTIONS += "--experimental-loader=extensionless";
36
+ throw new Error(`The current version of Node.js is not supported.
37
+ Supported versions are:
38
+ * greater than or equal to 20.6
39
+ * greater than or equal to 18.19 but less than 19`);
37
40
  }
38
41
  }
39
42
  async function main() {