@nuasite/notes 0.1.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.
Files changed (35) hide show
  1. package/README.md +211 -0
  2. package/dist/overlay.js +1367 -0
  3. package/package.json +51 -0
  4. package/src/apply/apply-suggestion.ts +157 -0
  5. package/src/dev/api-handlers.ts +215 -0
  6. package/src/dev/middleware.ts +65 -0
  7. package/src/dev/request-utils.ts +71 -0
  8. package/src/index.ts +2 -0
  9. package/src/integration.ts +168 -0
  10. package/src/overlay/App.tsx +434 -0
  11. package/src/overlay/components/CommentPopover.tsx +96 -0
  12. package/src/overlay/components/DiffPreview.tsx +29 -0
  13. package/src/overlay/components/ElementHighlight.tsx +33 -0
  14. package/src/overlay/components/SelectionTooltip.tsx +48 -0
  15. package/src/overlay/components/Sidebar.tsx +70 -0
  16. package/src/overlay/components/SidebarItem.tsx +104 -0
  17. package/src/overlay/components/StaleWarning.tsx +19 -0
  18. package/src/overlay/components/SuggestPopover.tsx +139 -0
  19. package/src/overlay/components/Toolbar.tsx +38 -0
  20. package/src/overlay/env.d.ts +4 -0
  21. package/src/overlay/index.tsx +71 -0
  22. package/src/overlay/lib/cms-bridge.ts +33 -0
  23. package/src/overlay/lib/dom-walker.ts +61 -0
  24. package/src/overlay/lib/manifest-fetch.ts +35 -0
  25. package/src/overlay/lib/notes-fetch.ts +121 -0
  26. package/src/overlay/lib/range-anchor.ts +87 -0
  27. package/src/overlay/lib/url-mode.ts +43 -0
  28. package/src/overlay/styles.css +526 -0
  29. package/src/overlay/types.ts +66 -0
  30. package/src/storage/id-gen.ts +32 -0
  31. package/src/storage/json-store.ts +196 -0
  32. package/src/storage/slug.ts +35 -0
  33. package/src/storage/types.ts +100 -0
  34. package/src/tsconfig.json +6 -0
  35. package/src/types.ts +50 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nuasite/notes",
3
+ "description": "Astro integration adding a Pastel-style comment + Google Docs-style suggestion overlay alongside @nuasite/cms.",
4
+ "files": [
5
+ "dist/**",
6
+ "src/**",
7
+ "README.md",
8
+ "package.json"
9
+ ],
10
+ "homepage": "https://github.com/nuasite/nuasite/blob/main/packages/notes/README.md",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/nuasite/nuasite.git",
14
+ "directory": "packages/notes"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "version": "0.1.0",
18
+ "module": "src/index.ts",
19
+ "types": "src/index.ts",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./src/index.ts",
24
+ "import": "./src/index.ts",
25
+ "default": "./src/index.ts"
26
+ }
27
+ },
28
+ "dependencies": {},
29
+ "devDependencies": {
30
+ "@types/bun": "1.3.11",
31
+ "astro": "6.1.4",
32
+ "preact": "^10.29.1"
33
+ },
34
+ "peerDependencies": {
35
+ "astro": "6.1.4",
36
+ "typescript": "^6.0.2"
37
+ },
38
+ "scripts": {
39
+ "build": "vite build --config vite.config.overlay.ts",
40
+ "prepack": "bun run build && bun run ../../scripts/workspace-deps/resolve-deps.ts"
41
+ },
42
+ "keywords": [
43
+ "astro",
44
+ "cms",
45
+ "devtools",
46
+ "nuasite",
47
+ "review",
48
+ "comments",
49
+ "suggestions"
50
+ ]
51
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Apply a suggestion's text replacement back to the source file.
3
+ *
4
+ * Each suggestion item already carries the source location it was created
5
+ * against (`targetSourcePath`, `targetSourceLine`) — both populated from the
6
+ * `@nuasite/cms` per-page manifest at create time. That gives us everything
7
+ * we need to perform the apply locally without peer-importing CMS internals:
8
+ *
9
+ * 1. Read the source file at `targetSourcePath`.
10
+ * 2. Find `range.originalText` in the file.
11
+ * - If it appears exactly once → replace it.
12
+ * - If it appears multiple times → use `targetSourceLine` to pick the
13
+ * nearest occurrence (within a small window). This handles repeated
14
+ * words / boilerplate inside large files.
15
+ * - If it doesn't appear → drift detected, return `stale`.
16
+ * 3. Atomically write the file back (write `.tmp`, rename).
17
+ *
18
+ * The Vite watcher inside CMS picks up the file write and triggers HMR,
19
+ * which reloads the page and shows the applied text. The notes API handler
20
+ * also fires its own HMR full-reload to be safe.
21
+ */
22
+
23
+ import fs from 'node:fs/promises'
24
+ import path from 'node:path'
25
+ import type { NoteItem } from '../storage/types'
26
+
27
+ export type ApplyResult =
28
+ | { ok: true; file: string; before: string; after: string }
29
+ | { ok: false; reason: 'not-suggestion' | 'missing-source' | 'file-error' | 'not-found' | 'ambiguous'; message: string }
30
+
31
+ interface ApplyOptions {
32
+ projectRoot: string
33
+ }
34
+
35
+ const LINE_WINDOW = 8
36
+
37
+ function* findAllOccurrences(haystack: string, needle: string): Generator<number> {
38
+ if (!needle) return
39
+ let from = 0
40
+ while (true) {
41
+ const idx = haystack.indexOf(needle, from)
42
+ if (idx < 0) return
43
+ yield idx
44
+ from = idx + needle.length
45
+ }
46
+ }
47
+
48
+ function lineOfOffset(content: string, offset: number): number {
49
+ let line = 1
50
+ for (let i = 0; i < offset && i < content.length; i++) {
51
+ if (content[i] === '\n') line++
52
+ }
53
+ return line
54
+ }
55
+
56
+ /**
57
+ * Resolve `targetSourcePath` to an absolute path inside the project root.
58
+ * Defends against path traversal: the resolved path must stay inside the
59
+ * project root.
60
+ */
61
+ function resolveSafe(projectRoot: string, sourcePath: string): string | null {
62
+ const root = path.resolve(projectRoot)
63
+ const candidate = path.resolve(root, sourcePath)
64
+ if (!candidate.startsWith(root + path.sep) && candidate !== root) return null
65
+ return candidate
66
+ }
67
+
68
+ export async function applySuggestion(item: NoteItem, options: ApplyOptions): Promise<ApplyResult> {
69
+ if (item.type !== 'suggestion' || !item.range) {
70
+ return { ok: false, reason: 'not-suggestion', message: 'Only suggestion items can be applied' }
71
+ }
72
+ if (!item.targetSourcePath) {
73
+ return {
74
+ ok: false,
75
+ reason: 'missing-source',
76
+ message: 'Suggestion is missing targetSourcePath. Cannot resolve which file to edit.',
77
+ }
78
+ }
79
+ const abs = resolveSafe(options.projectRoot, item.targetSourcePath)
80
+ if (!abs) {
81
+ return { ok: false, reason: 'missing-source', message: `Refusing to write outside project root: ${item.targetSourcePath}` }
82
+ }
83
+
84
+ let content: string
85
+ try {
86
+ content = await fs.readFile(abs, 'utf-8')
87
+ } catch (err) {
88
+ return {
89
+ ok: false,
90
+ reason: 'file-error',
91
+ message: `Could not read ${item.targetSourcePath}: ${err instanceof Error ? err.message : String(err)}`,
92
+ }
93
+ }
94
+
95
+ const original = item.range.originalText
96
+ const replacement = item.range.suggestedText
97
+ if (!original) {
98
+ return { ok: false, reason: 'not-found', message: 'Suggestion has empty originalText' }
99
+ }
100
+
101
+ const occurrences = Array.from(findAllOccurrences(content, original))
102
+ if (occurrences.length === 0) {
103
+ return {
104
+ ok: false,
105
+ reason: 'not-found',
106
+ message: `Original text not found in ${item.targetSourcePath}. Source may have drifted since the suggestion was made.`,
107
+ }
108
+ }
109
+
110
+ let chosenOffset: number
111
+ if (occurrences.length === 1) {
112
+ chosenOffset = occurrences[0]!
113
+ } else {
114
+ // Pick the occurrence closest to targetSourceLine (within a small window).
115
+ const targetLine = item.targetSourceLine ?? 0
116
+ let best: { offset: number; dist: number } | null = null
117
+ for (const off of occurrences) {
118
+ const ln = lineOfOffset(content, off)
119
+ const dist = Math.abs(ln - targetLine)
120
+ if (!best || dist < best.dist) best = { offset: off, dist }
121
+ }
122
+ if (!best || best.dist > LINE_WINDOW) {
123
+ return {
124
+ ok: false,
125
+ reason: 'ambiguous',
126
+ message:
127
+ `Original text appears ${occurrences.length} times in ${item.targetSourcePath} and none are near targetSourceLine ${targetLine}. Refusing to apply.`,
128
+ }
129
+ }
130
+ chosenOffset = best.offset
131
+ }
132
+
133
+ const before = content.slice(Math.max(0, chosenOffset - 40), chosenOffset + original.length + 40)
134
+ const newContent = content.slice(0, chosenOffset) + replacement + content.slice(chosenOffset + original.length)
135
+ const after = newContent.slice(Math.max(0, chosenOffset - 40), chosenOffset + replacement.length + 40)
136
+
137
+ // Atomic write — same pattern the JSON store uses.
138
+ const tmp = `${abs}.${process.pid}.${Date.now()}.tmp`
139
+ try {
140
+ await fs.writeFile(tmp, newContent, 'utf-8')
141
+ await fs.rename(tmp, abs)
142
+ } catch (err) {
143
+ // Clean up tmp on failure
144
+ try {
145
+ await fs.unlink(tmp)
146
+ } catch (cleanupErr) {
147
+ console.warn(`[nuasite-notes] Failed to clean up temp file ${tmp}:`, cleanupErr instanceof Error ? cleanupErr.message : cleanupErr)
148
+ }
149
+ return {
150
+ ok: false,
151
+ reason: 'file-error',
152
+ message: `Could not write ${item.targetSourcePath}: ${err instanceof Error ? err.message : String(err)}`,
153
+ }
154
+ }
155
+
156
+ return { ok: true, file: item.targetSourcePath, before, after }
157
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Dev API route handlers for `/_nua/notes/*`.
3
+ *
4
+ * All handlers operate on a single `NotesJsonStore` and follow the same
5
+ * shape as `@nuasite/cms`'s api-routes: parse query/body, mutate, send JSON.
6
+ *
7
+ * Routes (all mounted under `/_nua/notes/`):
8
+ *
9
+ * GET /list?page=/<page> → list items for one page
10
+ * GET /inbox → list items across all pages (Phase 5 use)
11
+ * POST /create → create a comment or suggestion
12
+ * POST /update → patch an existing item
13
+ * POST /resolve → mark item as resolved
14
+ * POST /reopen → reopen a resolved item
15
+ * POST /delete → delete an item
16
+ * POST /apply → write a suggestion's replacement back to source
17
+ */
18
+
19
+ import type { IncomingMessage, ServerResponse } from 'node:http'
20
+ import { applySuggestion } from '../apply/apply-suggestion'
21
+ import type { NotesJsonStore } from '../storage/json-store'
22
+ import type { NoteItem, NoteItemPatch, NoteRange, NoteStatus, NoteType } from '../storage/types'
23
+ import { parseJsonBody, sendError, sendJson } from './request-utils'
24
+
25
+ export interface NotesApiContext {
26
+ store: NotesJsonStore
27
+ projectRoot: string
28
+ }
29
+
30
+ interface CreateBody {
31
+ page: string
32
+ type: NoteType
33
+ targetCmsId: string
34
+ targetSourcePath?: string
35
+ targetSourceLine?: number
36
+ targetSnippet?: string
37
+ range?: NoteRange | null
38
+ body: string
39
+ author: string
40
+ }
41
+
42
+ interface UpdateBody {
43
+ page: string
44
+ id: string
45
+ patch: NoteItemPatch
46
+ }
47
+
48
+ interface IdBody {
49
+ page: string
50
+ id: string
51
+ }
52
+
53
+ function getQuery(url: string): URLSearchParams {
54
+ const q = url.indexOf('?')
55
+ return new URLSearchParams(q >= 0 ? url.slice(q + 1) : '')
56
+ }
57
+
58
+ export async function handleNotesApiRoute(
59
+ route: string,
60
+ req: IncomingMessage,
61
+ res: ServerResponse,
62
+ ctx: NotesApiContext,
63
+ ): Promise<void> {
64
+ const { store } = ctx
65
+ const method = req.method ?? 'GET'
66
+
67
+ // GET /list?page=/some-page
68
+ if (method === 'GET' && route === 'list') {
69
+ const params = getQuery(req.url ?? '')
70
+ const page = params.get('page')
71
+ if (!page) {
72
+ sendError(res, 'Missing required query param: page', 400, req)
73
+ return
74
+ }
75
+ const file = await store.readPage(page)
76
+ sendJson(res, file, 200, req)
77
+ return
78
+ }
79
+
80
+ // GET /inbox — all items across all pages
81
+ if (method === 'GET' && route === 'inbox') {
82
+ const pages = await store.listAllPages()
83
+ sendJson(res, { pages }, 200, req)
84
+ return
85
+ }
86
+
87
+ // POST /create
88
+ if (method === 'POST' && route === 'create') {
89
+ const body = await parseJsonBody<CreateBody>(req)
90
+ if (!body.page || !body.type || !body.targetCmsId || !body.author) {
91
+ sendError(res, 'Missing required fields: page, type, targetCmsId, author', 400, req)
92
+ return
93
+ }
94
+ if (body.type !== 'comment' && body.type !== 'suggestion') {
95
+ sendError(res, `Invalid type: ${body.type}`, 400, req)
96
+ return
97
+ }
98
+ if (body.type === 'suggestion' && !body.range) {
99
+ sendError(res, 'Suggestion items require a range payload', 400, req)
100
+ return
101
+ }
102
+ // Comments must have a body; suggestions may have an empty body since
103
+ // the diff itself communicates the change.
104
+ if (body.type === 'comment' && !body.body?.trim()) {
105
+ sendError(res, 'Comment items require a non-empty body', 400, req)
106
+ return
107
+ }
108
+ const item = await store.addItem(body.page, {
109
+ type: body.type,
110
+ targetCmsId: body.targetCmsId,
111
+ targetSourcePath: body.targetSourcePath,
112
+ targetSourceLine: body.targetSourceLine,
113
+ targetSnippet: body.targetSnippet,
114
+ range: body.range ?? null,
115
+ body: body.body ?? '',
116
+ author: body.author,
117
+ })
118
+ sendJson(res, { item }, 201, req)
119
+ return
120
+ }
121
+
122
+ // POST /update
123
+ if (method === 'POST' && route === 'update') {
124
+ const body = await parseJsonBody<UpdateBody>(req)
125
+ if (!body.page || !body.id || !body.patch) {
126
+ sendError(res, 'Missing required fields: page, id, patch', 400, req)
127
+ return
128
+ }
129
+ const updated = await store.updateItem(body.page, body.id, body.patch)
130
+ if (!updated) {
131
+ sendError(res, `Item not found: ${body.id}`, 404, req)
132
+ return
133
+ }
134
+ sendJson(res, { item: updated }, 200, req)
135
+ return
136
+ }
137
+
138
+ // POST /resolve and POST /reopen — convenience wrappers around update
139
+ if (method === 'POST' && (route === 'resolve' || route === 'reopen')) {
140
+ const body = await parseJsonBody<IdBody>(req)
141
+ if (!body.page || !body.id) {
142
+ sendError(res, 'Missing required fields: page, id', 400, req)
143
+ return
144
+ }
145
+ const status: NoteStatus = route === 'resolve' ? 'resolved' : 'open'
146
+ const updated = await store.updateItem(body.page, body.id, { status })
147
+ if (!updated) {
148
+ sendError(res, `Item not found: ${body.id}`, 404, req)
149
+ return
150
+ }
151
+ sendJson(res, { item: updated }, 200, req)
152
+ return
153
+ }
154
+
155
+ // POST /delete
156
+ if (method === 'POST' && route === 'delete') {
157
+ const body = await parseJsonBody<IdBody>(req)
158
+ if (!body.page || !body.id) {
159
+ sendError(res, 'Missing required fields: page, id', 400, req)
160
+ return
161
+ }
162
+ const ok = await store.deleteItem(body.page, body.id)
163
+ if (!ok) {
164
+ sendError(res, `Item not found: ${body.id}`, 404, req)
165
+ return
166
+ }
167
+ sendJson(res, { ok: true }, 200, req)
168
+ return
169
+ }
170
+
171
+ // POST /apply — write the suggestion's replacement back to the source file
172
+ if (method === 'POST' && route === 'apply') {
173
+ const body = await parseJsonBody<IdBody>(req)
174
+ if (!body.page || !body.id) {
175
+ sendError(res, 'Missing required fields: page, id', 400, req)
176
+ return
177
+ }
178
+ const file = await store.readPage(body.page)
179
+ const item = file.items.find(it => it.id === body.id)
180
+ if (!item) {
181
+ sendError(res, `Item not found: ${body.id}`, 404, req)
182
+ return
183
+ }
184
+ if (item.type !== 'suggestion' || !item.range) {
185
+ sendError(res, 'Only suggestion items can be applied', 400, req)
186
+ return
187
+ }
188
+
189
+ const result = await applySuggestion(item, { projectRoot: ctx.projectRoot })
190
+
191
+ if (!result.ok) {
192
+ // Drift detected — mark the item as stale so the sidebar can warn
193
+ // the agency without losing the suggestion.
194
+ if (result.reason === 'not-found' || result.reason === 'ambiguous') {
195
+ const updated = await store.updateItem(body.page, body.id, { status: 'stale' })
196
+ sendJson(res, { item: updated, error: result.message, reason: result.reason }, 409, req)
197
+ return
198
+ }
199
+ sendError(res, result.message, 400, req)
200
+ return
201
+ }
202
+
203
+ // Successful write — flip the item to `applied`. The middleware will
204
+ // fire a Vite full-reload after this returns; CMS's own watcher also
205
+ // notices the source-file change and triggers HMR.
206
+ const updated = await store.updateItem(body.page, body.id, { status: 'applied' })
207
+ sendJson(res, { item: updated, file: result.file, before: result.before, after: result.after }, 200, req)
208
+ return
209
+ }
210
+
211
+ sendError(res, `Unknown notes route: ${method} ${route}`, 404, req)
212
+ }
213
+
214
+ // Re-export for tests / external use
215
+ export type { NoteItem }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Vite dev middleware mounting `/_nua/notes/*`.
3
+ *
4
+ * Mirrors the structure of `@nuasite/cms`'s `createDevMiddleware`:
5
+ * - Filters by URL prefix and short-circuits other requests
6
+ * - Handles CORS preflight before dispatching
7
+ * - Catches handler errors and surfaces them as 500 JSON responses
8
+ * - Triggers a Vite HMR full-reload after content-modifying POSTs so
9
+ * pages currently open in a browser reflect the new note immediately
10
+ */
11
+
12
+ import type { IncomingMessage, ServerResponse } from 'node:http'
13
+ import type { NotesJsonStore } from '../storage/json-store'
14
+ import type { NotesApiContext } from './api-handlers'
15
+ import { handleNotesApiRoute } from './api-handlers'
16
+ import { handleCors, sendError } from './request-utils'
17
+
18
+ /** Minimal ViteDevServer interface — same shape used by `@nuasite/cms`. */
19
+ export interface ViteDevServerLike {
20
+ middlewares: {
21
+ use: (middleware: (req: IncomingMessage, res: ServerResponse, next: () => void) => void) => void
22
+ }
23
+ ws?: {
24
+ send: (payload: { type: string; path?: string }) => void
25
+ }
26
+ }
27
+
28
+ const ROUTE_PREFIX = '/_nua/notes/'
29
+
30
+ export function createNotesDevMiddleware(
31
+ server: ViteDevServerLike,
32
+ store: NotesJsonStore,
33
+ projectRoot: string,
34
+ ): void {
35
+ const ctx: NotesApiContext = { store, projectRoot }
36
+
37
+ server.middlewares.use((req, res, next) => {
38
+ const url = req.url ?? ''
39
+ if (!url.startsWith(ROUTE_PREFIX)) {
40
+ next()
41
+ return
42
+ }
43
+
44
+ if (handleCors(req, res)) return
45
+
46
+ const route = url.replace(ROUTE_PREFIX, '').split('?')[0] ?? ''
47
+
48
+ handleNotesApiRoute(route, req, res, ctx)
49
+ .then(() => {
50
+ // Mirror CMS: explicitly trigger full-reload after content-modifying
51
+ // routes. In sandboxed dev environments (E2B etc.) chokidar events
52
+ // may not fire reliably for note JSON files, so we send the HMR
53
+ // event directly. The overlay re-fetches `/list` on reload.
54
+ if (req.method === 'POST' && server.ws) {
55
+ server.ws.send({ type: 'full-reload' })
56
+ }
57
+ })
58
+ .catch((error) => {
59
+ console.error('[nuasite-notes] API error:', error)
60
+ if (!res.headersSent) {
61
+ sendError(res, error instanceof Error ? error.message : 'Internal server error', 500, req)
62
+ }
63
+ })
64
+ })
65
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tiny HTTP helpers for the notes dev API. Mirrors `@nuasite/cms`'s
3
+ * `handlers/request-utils.ts` but kept local so notes has no runtime
4
+ * dependency on CMS internals (only on the published source-finder
5
+ * surface, used in Phase 4).
6
+ */
7
+
8
+ import type { IncomingMessage, ServerResponse } from 'node:http'
9
+
10
+ const MAX_BODY_SIZE = 2 * 1024 * 1024 // 2 MB — notes are text, no uploads
11
+
12
+ export function readBody(req: IncomingMessage, maxSize: number = MAX_BODY_SIZE): Promise<Buffer> {
13
+ return new Promise((resolve, reject) => {
14
+ const chunks: Buffer[] = []
15
+ let totalSize = 0
16
+ req.on('data', (chunk: Buffer) => {
17
+ totalSize += chunk.length
18
+ if (totalSize > maxSize) {
19
+ req.destroy()
20
+ reject(new Error(`Request body exceeds maximum size of ${maxSize} bytes`))
21
+ return
22
+ }
23
+ chunks.push(chunk)
24
+ })
25
+ req.on('end', () => resolve(Buffer.concat(chunks)))
26
+ req.on('error', reject)
27
+ })
28
+ }
29
+
30
+ export async function parseJsonBody<T>(req: IncomingMessage): Promise<T> {
31
+ const buf = await readBody(req)
32
+ if (buf.length === 0) return {} as T
33
+ try {
34
+ return JSON.parse(buf.toString('utf-8')) as T
35
+ } catch {
36
+ throw new Error('Invalid JSON in request body')
37
+ }
38
+ }
39
+
40
+ function getCorsOrigin(req: IncomingMessage): string {
41
+ return req.headers.origin ?? '*'
42
+ }
43
+
44
+ export function sendJson(res: ServerResponse, data: unknown, status = 200, req?: IncomingMessage): void {
45
+ res.writeHead(status, {
46
+ 'Content-Type': 'application/json',
47
+ 'Cache-Control': 'no-store',
48
+ 'Access-Control-Allow-Origin': req ? getCorsOrigin(req) : '*',
49
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
50
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
51
+ })
52
+ res.end(JSON.stringify(data))
53
+ }
54
+
55
+ export function sendError(res: ServerResponse, message: string, status = 400, req?: IncomingMessage): void {
56
+ sendJson(res, { error: message }, status, req)
57
+ }
58
+
59
+ export function handleCors(req: IncomingMessage, res: ServerResponse): boolean {
60
+ if (req.method === 'OPTIONS') {
61
+ res.writeHead(204, {
62
+ 'Access-Control-Allow-Origin': getCorsOrigin(req),
63
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
64
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
65
+ 'Access-Control-Max-Age': '86400',
66
+ })
67
+ res.end()
68
+ return true
69
+ }
70
+ return false
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default, nuaNotes } from './integration'
2
+ export type { NuaNotesOptions } from './types'