@trypattern/sdk 0.1.19 → 0.1.21

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.
@@ -5,5 +5,12 @@ export type AuthService = {
5
5
  exchange: (code: string) => Promise<ExchangeReply | undefined>;
6
6
  sendCode: (request: SendCodeRequest) => Promise<SendCodeReply | undefined>;
7
7
  verifyCode: (request: VerifyCodeRequest) => Promise<VerifiedIdentity | undefined>;
8
+ noteOwner: (note: OwnerNote) => Promise<boolean>;
9
+ };
10
+ export type OwnerNote = {
11
+ subject: string;
12
+ text: string;
13
+ replyTo?: string;
8
14
  };
9
15
  export declare function authServiceFor(env: ServerBindings): AuthService | undefined;
16
+ export declare function notifyOwner(env: ServerBindings, note: OwnerNote): Promise<boolean>;
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import { exchangeReply, sendCodeReply, verifyCodeReply } from "./signInWire.js";
2
3
  export const AUTH_URL = 'https://auth.trypattern.ai';
3
4
  function httpAuthService(secret) {
@@ -18,7 +19,9 @@ function httpAuthService(secret) {
18
19
  return {
19
20
  exchange: (code) => post('/exchange', { code }, exchangeReply),
20
21
  sendCode: (request) => post('/email/send', request, sendCodeReply),
21
- verifyCode: async (request) => (await post('/email/verify', request, verifyCodeReply))?.identity
22
+ verifyCode: async (request) => (await post('/email/verify', request, verifyCodeReply))?.identity,
23
+ noteOwner: async (note) => (await post('/owner-note', note, z.object({ ok: z.literal(true) }))) !==
24
+ undefined
22
25
  };
23
26
  }
24
27
  const devCodes = new Map();
@@ -41,6 +44,10 @@ const devAuthService = {
41
44
  return undefined;
42
45
  devCodes.delete(challenge);
43
46
  return { email: sent.email, name: null, avatarUrl: null, via: 'email' };
47
+ },
48
+ noteOwner: async ({ subject, text }) => {
49
+ console.log(`Email to the owner: ${subject}\n${text}`);
50
+ return true;
44
51
  }
45
52
  };
46
53
  export function authServiceFor(env) {
@@ -48,3 +55,11 @@ export function authServiceFor(env) {
48
55
  return httpAuthService(env.AUTH_SECRET);
49
56
  return import.meta.env.DEV ? devAuthService : undefined;
50
57
  }
58
+ export async function notifyOwner(env, note) {
59
+ try {
60
+ return (await authServiceFor(env)?.noteOwner(note)) ?? false;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
@@ -3,10 +3,11 @@ import type { BookingRequest } from '../bookings.ts';
3
3
  import type { ServerBlock, ServerEnv } from './env.ts';
4
4
  export type BookingsOptions = {
5
5
  timezone: string;
6
+ managePath?: string;
6
7
  };
7
8
  export type BookingEmail = {
8
9
  kind: 'requested' | 'accepted' | 'declined';
9
10
  request: BookingRequest;
10
11
  };
11
12
  export declare function sendBookingEmail(c: Context<ServerEnv>, email: BookingEmail): Promise<void>;
12
- export declare function bookings({ timezone }: BookingsOptions): ServerBlock;
13
+ export declare function bookings({ timezone, managePath }: BookingsOptions): ServerBlock;
@@ -1,5 +1,6 @@
1
1
  import { FULL, RULES_ID, UNAVAILABLE, bookingBlackout, bookingRequests, bookingRules, bookingServices, bookingTables, daysOf, isValidTimezone, rulesOf, rulesProblem, slotFor, takenKey, windowsOn } from "../bookings.js";
2
2
  import { refused } from "../services.js";
3
+ import { notifyOwner } from "./authService.js";
3
4
  import { serveServices } from "./services.js";
4
5
  import { readRow, readRows } from "./tables.js";
5
6
  const HOLDING_SQL = "status IN ('requested', 'accepted')";
@@ -9,6 +10,39 @@ export async function sendBookingEmail(c, email) {
9
10
  const to = email.kind === 'requested' ? 'the owner' : email.request.email;
10
11
  console.log(`Booking email (${email.kind}) to ${to} from ${new URL(c.req.url).host}: ${email.request.name}, ${email.request.date} ${email.request.window}`);
11
12
  }
13
+ function clockOf(time) {
14
+ const [hours = 0, minutes = 0] = time.split(':').map(Number);
15
+ const hour = hours % 12 === 0 ? 12 : hours % 12;
16
+ const suffix = hours < 12 ? 'am' : 'pm';
17
+ return minutes === 0
18
+ ? `${hour}${suffix}`
19
+ : `${hour}:${String(minutes).padStart(2, '0')}${suffix}`;
20
+ }
21
+ function whenOf(request, label) {
22
+ const day = new Date(`${request.date}T12:00:00Z`).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' });
23
+ const [start = '', end = ''] = request.window.split('-');
24
+ return `${day}, ${label} (${clockOf(start)}–${clockOf(end)})`;
25
+ }
26
+ function ownerNoteOf(request, label, manage) {
27
+ const lines = [
28
+ `${request.name} asked to book ${request.offering} on ${whenOf(request, label)}.`,
29
+ '',
30
+ `Email: ${request.email}`,
31
+ ...(request.phone === '' ? [] : [`Phone: ${request.phone}`]),
32
+ ...(request.address === '' ? [] : [`Address: ${request.address}`]),
33
+ ...(request.notes === '' ? [] : ['', request.notes]),
34
+ '',
35
+ request.status === 'accepted'
36
+ ? `It is booked: your booking rules accept requests on their own. See it at ${manage}`
37
+ : `Accept or decline it at ${manage}`,
38
+ `Reply to this email to write to ${request.name}.`
39
+ ];
40
+ return {
41
+ subject: `New booking request from ${request.name}`,
42
+ text: lines.join('\n'),
43
+ replyTo: request.email
44
+ };
45
+ }
12
46
  function requireAdmin(c) {
13
47
  const viewer = c.get('viewer');
14
48
  if (viewer === null)
@@ -65,7 +99,7 @@ async function decide(c, id, status) {
65
99
  function monthRange(month) {
66
100
  return { first: `${month}-01`, last: `${month}-31` };
67
101
  }
68
- export function bookings({ timezone }) {
102
+ export function bookings({ timezone, managePath = '/' }) {
69
103
  if (!isValidTimezone(timezone)) {
70
104
  throw new Error(`bookings({ timezone: '${timezone}' }) is not a time zone: use an IANA name such as 'America/Toronto'`);
71
105
  }
@@ -97,6 +131,7 @@ export function bookings({ timezone }) {
97
131
  const saved = await readRequest(c.env.DB, id);
98
132
  if (saved === undefined)
99
133
  throw new Error('The request was not saved');
134
+ await notifyOwner(c.env, ownerNoteOf(saved, slot.label, new URL(managePath, c.req.url).href));
100
135
  await sendBookingEmail(c, {
101
136
  kind: status === 'accepted' ? 'accepted' : 'requested',
102
137
  request: saved
@@ -1,5 +1,7 @@
1
1
  export { createServer } from './createServer.ts';
2
2
  export type { ServerBindings, ServerBlock, ServerEnv, Viewer } from './env.ts';
3
+ export { notifyOwner } from './authService.ts';
4
+ export type { OwnerNote } from './authService.ts';
3
5
  export { bookings } from './bookings.ts';
4
6
  export type { BookingsOptions } from './bookings.ts';
5
7
  export { seedSql, serveTables, sqlTable, sqlTables } from './tables.ts';
@@ -1,4 +1,5 @@
1
1
  export { createServer } from "./createServer.js";
2
+ export { notifyOwner } from "./authService.js";
2
3
  export { bookings } from "./bookings.js";
3
4
  export { seedSql, serveTables, sqlTable, sqlTables } from "./tables.js";
4
5
  export { refused } from "../services.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",