h1v3 0.7.0 → 0.7.2

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.
@@ -20,21 +20,20 @@ function defaultHandleError(err) {
20
20
 
21
21
  }
22
22
 
23
- export async function inject({ database, configPath, handleError = defaultHandleError }) {
23
+ export async function inject({ database, authentication, metaURL, handleError = defaultHandleError }) {
24
24
 
25
25
  if (!database) throw new Error("Missing database");
26
- if (!configPath) throw new Error("Missing configPath");
27
-
28
- const { db, ref, get } = database;
29
- const snap = await get(ref(db, configPath));
30
- const config = snap.val();
31
-
32
- const log = logger(database, config.logs.path);
33
- const teamMembershipStoreFactory = ({ tid }) => eventStore(database, populateParameters({ tid }, config.team.membership.path));
34
- const teamDetailsStoreFactory = ({ tid }) => eventStore(database, populateParameters({ tid }, config.team.details.path));
35
- const userInvitesStoreFactory = ({ emailId }) => eventStore(database, populateParameters({ emailId }, config.userInvites.path));
36
- const userTeamsStoreFactory = ({ uid }) => eventStore(database, populateParameters({ uid }, config.user.teams.path));
37
- const userProfileStoreFactory = ({ uid }) => eventStore(database, populateParameters({ uid }, config.user.profile.path));
26
+ if (!metaURL) throw new Error("Missing metaURL");
27
+ if (!authentication) throw new Error("Missing authentication");
28
+
29
+ const meta = await fetch(metaURL).then(resp => resp.json())
30
+
31
+ const log = logger(database, authentication, meta.o11y.path);
32
+ const teamMembershipStoreFactory = ({ tid }) => eventStore(database, authentication, populateParameters({ tid }, meta.team.membership.path));
33
+ const teamDetailsStoreFactory = ({ tid }) => eventStore(database, authentication, populateParameters({ tid }, meta.team.details.path));
34
+ const userInvitesStoreFactory = ({ emailId }) => eventStore(database, authentication, populateParameters({ emailId }, meta.userInvites.path));
35
+ const userTeamsStoreFactory = ({ uid }) => eventStore(database, authentication, populateParameters({ uid }, meta.user.teams.path));
36
+ const userProfileStoreFactory = ({ uid }) => eventStore(database, authentication, populateParameters({ uid }, meta.user.profile.path));
38
37
 
39
38
  return {
40
39
 
@@ -5,11 +5,13 @@ const newEventId = eventType => `${timeId()}-${eventType}`;
5
5
  export const PROJECTIONS = "_p";
6
6
  export const EVENTS = "_e";
7
7
 
8
- export function eventStore(database, path) {
8
+ export function eventStore(database, authentication, path) {
9
9
 
10
10
  const { db, ref, onValue, get, set } = database;
11
11
  for (const required of ["db", "ref", "onValue", "get", "set"])
12
12
  if (!database[required]) throw new Error(`Missing required database.${required}`);
13
+ const { auth } = authentication;
14
+ if (!auth) throw new Error("Missing required authentication.auth");
13
15
 
14
16
  const eventsPath = `${path}/${EVENTS}`;
15
17
  const projectionsPath = `${path}/${PROJECTIONS}`;
@@ -46,8 +48,13 @@ export function eventStore(database, path) {
46
48
  async record(eventType, payload) {
47
49
 
48
50
  const eid = newEventId(eventType);
49
- const data = { type: eventType, payload, meta: { when: new Date().toISOString() } };
50
- await set(ref(db, `${eventsPath}/${eid}`), data);
51
+ const meta = {
52
+ when: new Date().toISOString(),
53
+ uid: authentication.auth?.currentUser?.uid
54
+ };
55
+ const data = { type: eventType, payload, meta };
56
+ const path = `${eventsPath}/${eid}`;
57
+ await set(ref(db, path), data);
51
58
 
52
59
  },
53
60
 
@@ -11,22 +11,16 @@ function munge(attributes) {
11
11
 
12
12
  }
13
13
 
14
- export function logger(database, path, defaultAttributes) {
14
+ export function logger(database, authentication, path, defaultAttributes) {
15
15
 
16
16
  const { db, ref, set } = database;
17
- const record = data => {
18
-
19
- const refPath = path.replace("$id", timeId());
20
- console.log("Logging", data, "to", refPath);
21
- return set(ref(db, refPath), data);
22
-
23
- };
24
17
 
25
18
  async function recordLog(trace_id, level, sharedAttributes, { body, parent_id, attributes }) {
26
19
 
27
20
  const span_id = generateSpanId();
28
21
  console.log(defaultAttributes, sharedAttributes, attributes);
29
22
  const data = {
23
+ meta: { uid: authentication.auth.currentUser.uid },
30
24
  body,
31
25
  trace_id,
32
26
  span_id,
@@ -35,7 +29,10 @@ export function logger(database, path, defaultAttributes) {
35
29
  attributes: munge({ ...defaultAttributes, ...sharedAttributes, ...attributes })
36
30
  };
37
31
  if (parent_id) data.parent_id = parent_id;
38
- await record(data);
32
+ const refPath = path.replace("$id", timeId());
33
+ console.log("Logging", data, "to", refPath);
34
+ return await set(ref(db, refPath), data);
35
+
39
36
 
40
37
  }
41
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1v3",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -4,6 +4,7 @@ import { user } from "../../dist/browser/client/team/user.js";
4
4
 
5
5
  export function inject({
6
6
  db,
7
+ uid: authorUid,
7
8
  teamMembershipConfig,
8
9
  userTeamsConfig,
9
10
  userInvitesConfig,
@@ -16,14 +17,15 @@ export function inject({
16
17
  if (!userInvitesConfig) throw new Error("Missing: userInvitesConfig");
17
18
  if (!teamDetailsConfig) throw new Error("Missing: teamDetailsConfig");
18
19
  if (!userProfileConfig) throw new Error("Missing: userProfileConfig");
19
- if (!db) throw new Error("Missing: db");
20
+ if (!db || (typeof db !== "object")) throw new Error("Missing object: db");
21
+ if (!authorUid || (typeof authorUid !== "string")) throw new Error("Missing string: uid");
20
22
 
21
23
  return {
22
24
 
23
25
  team({ tid }) {
24
26
 
25
- const teamMembershipStore = eventStoreFromConfig(db, teamMembershipConfig, { tid });
26
- const teamDetailsStore = eventStoreFromConfig(db, teamDetailsConfig, { tid });
27
+ const teamMembershipStore = eventStoreFromConfig(db, authorUid, teamMembershipConfig, { tid });
28
+ const teamDetailsStore = eventStoreFromConfig(db, authorUid, teamDetailsConfig, { tid });
27
29
  return team({
28
30
  teamMembershipStore,
29
31
  teamDetailsStore,
@@ -35,8 +37,8 @@ export function inject({
35
37
 
36
38
  userProfile({ uid }) {
37
39
 
38
- const userTeamsStore = eventStoreFromConfig(db, userTeamsConfig, { uid });
39
- const userProfileStore = eventStoreFromConfig(db, userProfileConfig, { uid });
40
+ const userTeamsStore = eventStoreFromConfig(db, authorUid, userTeamsConfig, { uid });
41
+ const userProfileStore = eventStoreFromConfig(db, authorUid, userProfileConfig, { uid });
40
42
  return user({ userTeamsStore, userProfileStore });
41
43
 
42
44
  }
@@ -45,13 +47,13 @@ export function inject({
45
47
 
46
48
  function userTeamsStoreFactory({ uid }) {
47
49
 
48
- return eventStoreFromConfig(db, userTeamsConfig, { uid });
50
+ return eventStoreFromConfig(db, authorUid, userTeamsConfig, { uid });
49
51
 
50
52
  }
51
53
 
52
54
  function userInvitesStoreFactory({ emailId }) {
53
55
 
54
- return eventStoreFromConfig(db, userInvitesConfig, { emailId });
56
+ return eventStoreFromConfig(db, authorUid, userInvitesConfig, { emailId });
55
57
 
56
58
  }
57
59
 
@@ -1,7 +1,7 @@
1
1
  import { EVENT_STORE_META } from "../event-store/configuration.js";
2
2
  import { eventStore as eventStoreModular } from "../../dist/browser/client/event-store.js";
3
3
 
4
- function adapter(db) {
4
+ function adaptDatabase(db) {
5
5
 
6
6
  return {
7
7
  db,
@@ -42,16 +42,33 @@ function resolvePath(pathTemplate, pathValues) {
42
42
 
43
43
  }
44
44
 
45
- export function eventStoreFromConfig(db, config, pathValues = {}) {
45
+ export function eventStoreFromConfig(db, uid, config, pathValues = {}) {
46
+
47
+ if (!db || typeof db !== "object") throw new Error(`Expected object for db: ${db}`);
48
+ if (!uid || typeof uid !== "string") throw new Error(`Expected string for uid: ${uid}`);
49
+ if (!config || !(EVENT_STORE_META in config)) throw new Error(`Expected config tagged with event store metadata: ${config}`);
46
50
 
47
51
  const meta = config[EVENT_STORE_META];
48
- return eventStore(db, resolvePath(meta.ref, pathValues));
52
+ return eventStore(db, uid, resolvePath(meta.ref, pathValues));
49
53
 
50
54
  }
51
55
 
52
- export function eventStore(db, path) {
56
+ export function eventStore(db, uid, path) {
57
+
58
+ if (!db || typeof db !== "object") throw new Error(`Expected object for db: ${db}`);
59
+ if (!uid || typeof uid !== "string") throw new Error(`Expected string for uid: ${uid}`);
60
+ if (!path || typeof path !== "string") throw new Error(`Expected string for path: ${path}`);
53
61
 
54
- const firebase = adapter(db);
55
- return eventStoreModular(firebase, path);
62
+ return eventStoreModular(
63
+ adaptDatabase(db),
64
+ adaptAuthentication(uid),
65
+ path
66
+ );
67
+
68
+ }
56
69
 
57
- }
70
+ function adaptAuthentication(uid) {
71
+
72
+ return { auth: { currentUser: { uid } } };
73
+
74
+ }
@@ -1,10 +1,11 @@
1
1
  import { EVENTS, PROJECTIONS } from "../../dist/browser/client/event-store.js";
2
2
  import { explodePathVars } from "../index.js";
3
+ import { writeFileSync } from "node:fs";
3
4
 
4
5
  function writeConditions(config) {
5
6
 
6
7
  if (config.write === false) return false;
7
- let expr = "auth.uid != null && !data.exists()";
8
+ let expr = "(auth.uid!=null) && !data.exists() && (newData.child('meta/uid').val()==auth.uid)";
8
9
  if (config?.write)
9
10
  expr += " && (" + explodePathVars(config.write) + ")"
10
11
  return expr;
@@ -93,18 +94,29 @@ const parseConfig = ([_name, config]) => [
93
94
  config
94
95
  ];
95
96
 
96
- export function generateRules(_argv, eventStores, rawStores) {
97
+ export function generateRules(argv, eventStores, rawStores) {
97
98
 
98
- console.log(
99
- JSON.stringify(
99
+
100
+ const rulesJSON = JSON.stringify(
100
101
  {
101
102
  rules: [
102
103
  ...eventStores.map(parseConfig),
103
104
  ...rawStores.map(parseConfig)
104
105
  ].reduce(addRulesForPath, {})
105
106
  },
106
- null, 4)
107
+ null,
108
+ 4
107
109
  );
110
+ if (argv.output) {
111
+
112
+ console.log("Writing to", argv.output);
113
+ writeFileSync(argv.output, rulesJSON);
114
+
115
+ } else {
116
+
117
+ console.log(rulesJSON);
118
+
119
+ }
108
120
 
109
121
  }
110
122
 
@@ -1,3 +1,4 @@
1
+ import { loadConfigurationFromModule } from "./load-configuration.js";
1
2
  import { refPathToTriggerPath } from "./paths.js";
2
3
  import { all, assertNewDataDoesNotHave, assertNewDataHasString, any, assertNewDataHasNumber, assertNewDataHasOneOf, assertNewDataHasLowercaseHexCharacters, assertNewDataFieldDoesNotMatch } from "./rules.js";
3
4
 
@@ -17,19 +18,72 @@ export function configureConfigStore(paths, _onValueWritten, _observe) {
17
18
 
18
19
  }
19
20
 
20
- function decideSeverity(newData) {
21
+ function buildConfigDigest(module, paths) {
22
+
23
+
24
+ const refConfigMap =
25
+ loadConfigurationFromModule(module)
26
+ .flatMap(x => x.map(y => y[1]))
27
+ .reduce((index, config) => ({ ...index, [config.ref]: config }), {});
28
+
29
+ function patchForPath(map) {
30
+
31
+ const ret = {
32
+ ...map
33
+ };
34
+ const mappedConfig = refConfigMap[map.path];
35
+ for (const [x, y] of Object.entries(ret).filter(([_, v]) => typeof v === "object")) {
36
+
37
+ ret[x] = patchForPath(y);
38
+
39
+ }
40
+ if (mappedConfig && (mappedConfig.read || mappedConfig.write)) {
21
41
 
22
- const text = newData.severity_text?.toLowerCase();
23
- return ["trace", "fatal", "info", "debug", "error", "warn"].includes(text) ? text : "info";
42
+ ret.projections = mappedConfig.projections;
43
+ ret.eventTypes = mappedConfig.eventTypes;
44
+
45
+ }
46
+ return ret;
47
+
48
+ }
49
+
50
+ return patchForPath(paths);
24
51
 
25
52
  }
26
53
 
27
- function decideMessage(newData) {
54
+ const publicFor1Day = "public, max-age=86400";
55
+
56
+ export function configureMetadataEndpoint({ module, paths, onRequest, region, cacheControl = publicFor1Day }) {
28
57
 
29
- return newData?.body || "Log message without body";
58
+ const digest = buildConfigDigest(module, paths);
59
+ return onRequest(
60
+ { region },
61
+ async (_, res) => {
62
+
63
+ res.header("Cache-Control", cacheControl).send(digest);
64
+
65
+ }
66
+ );
67
+
68
+ }
69
+
70
+ const googleLogLevels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY"];
71
+
72
+ function decideSeverity(newData) {
73
+
74
+ const text = newData.severity_text?.toUpperCase();
75
+ return googleLogLevels.includes(text) ? text : "INFO";
30
76
 
31
77
  }
32
78
 
79
+ /*
80
+
81
+ message, severity, // required
82
+ traceId, spanId, timestamp, // advised
83
+ logData, // optional log line
84
+ spanStart, spanEnd, spanAttributes // optional trace attributes
85
+
86
+ */
33
87
  function handleO11yEventWritten(observe, e) {
34
88
 
35
89
  const { after } = e.data;
@@ -39,16 +93,21 @@ function handleO11yEventWritten(observe, e) {
39
93
  const now = new Date();
40
94
  const time_server_ms = now.valueOf();
41
95
  const timestamp = now.toISOString();
96
+ const message = newData?.message || "Missing log message";
97
+
98
+ const { traceId, spanId, ...rest } = newData
99
+ const logData = { ...rest, time_server_ms };
100
+
42
101
  observe({
43
- "logging.googleapis.com/trace": `projects/${process.env.GCLOUD_PROJECT}/traces/${newData.trace_id}`,
44
- "logging.googleapis.com/spanId": newData.span_id,
45
- "message": decideMessage(newData),
46
- "severity": decideSeverity(newData),
47
- "raw": {
48
- ...newData,
49
- time_server_ms
50
- },
51
- timestamp
102
+ message,
103
+ severity: decideSeverity(newData),
104
+
105
+ traceId: newData.trace_id,
106
+ spanId: newData.span_id,
107
+ timestamp,
108
+
109
+ logData
110
+
52
111
  });
53
112
  after.ref.update({ time_server_ms });
54
113
 
@@ -81,7 +140,7 @@ export function configureLoggingStore({ paths, onValueWritten, observe, region,
81
140
  write: all(
82
141
  // required
83
142
  assertNewDataHasString("body", 500, 1),
84
- assertNewDataHasOneOf("severity_text", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"),
143
+ assertNewDataHasOneOf("severity_text", ...googleLogLevels),
85
144
  // trace context
86
145
  all(
87
146
  assertNewDataHasLowercaseHexCharacters("trace_id", 32),
@@ -6,8 +6,8 @@ export const EVENT_STORE_META = Symbol("Event store configuration metadata");
6
6
 
7
7
  export function configureEventStore({ ref, projections, ...rest }, { onValueWritten, observe, region, instance }) {
8
8
 
9
- if (!onValueWritten) throw new Error("Missing onValueWritten");
10
- if (!observe) throw new Error("Missing observe");
9
+ if (typeof onValueWritten !== "function") throw new Error(`Missing onValueWritten function: ${onValueWritten}`);
10
+ if (typeof observe !== "function") throw new Error(`Missing observe function: ${observe}`);
11
11
  if (!region) throw new Error("Missing region");
12
12
  if (!instance) throw new Error("Missing instance");
13
13
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { listEventStores } from "./commands/list-event-stores.js";
4
4
  import { generateRules } from "./commands/generate-rules.js";
@@ -35,38 +35,6 @@ main({
35
35
  strategy: generateRules
36
36
 
37
37
  },
38
- // "vendor-eventstore": {
39
-
40
- // description: "Copy event store client into your browser assets folder",
41
- // loadConfig: false,
42
- // parameters: {},
43
- // strategy: vendor.eventStore
44
-
45
- // },
46
- // "vendor-web": {
47
-
48
- // description: "Copy web logic platform into your browser assets folder",
49
- // loadConfig: false,
50
- // parameters: {},
51
- // strategy: vendor.webPlatform
52
-
53
- // },
54
- // "vendor-webui": {
55
-
56
- // description: "Copy web UI platform into your browser assets folder (requires web)",
57
- // loadConfig: false,
58
- // parameters: {},
59
- // strategy: vendor.webPlatformUI
60
-
61
- // },
62
- // "vendor-membership": {
63
-
64
- // description: "Copy team management logic into your browser assets folder",
65
- // loadConfig: false,
66
- // parameters: {},
67
- // strategy: vendor.membership
68
-
69
- // },
70
38
  "vendor-link": {
71
39
 
72
40
  description: "Symlink latest vendored h1v3 assets as h1v3@latest",
package/src/index.js CHANGED
@@ -33,4 +33,6 @@ export function explodePathVars(expr) {
33
33
 
34
34
  }
35
35
 
36
- export * as o11y from "./observability/index.js";
36
+ export * as o11y from "./observability/index.js";
37
+
38
+ export { loadConfigurationFromModule } from "./load-configuration.js";
@@ -9,11 +9,15 @@ export async function loadConfiguration(argv) {
9
9
  if (!argv.snippet)
10
10
  console.log("Loading configuration:", configPath);
11
11
  const module = await import(pathToFileURL(configPath));
12
+ return loadConfigurationFromModule(module)
13
+
14
+ }
15
+
16
+ export function loadConfigurationFromModule(module) {
12
17
  return [
13
18
  readStoresMetadata(module),
14
19
  readRawMetadata(module)
15
- ]
16
-
20
+ ];
17
21
  }
18
22
 
19
23
  export function readRawMetadata(config) {
@@ -1,4 +1,4 @@
1
- export async function provider({ trace, projectId, serviceName, log }) {
1
+ export function provider({ trace, projectId, serviceName, log }) {
2
2
 
3
3
  if (!serviceName) throw new Error("Missing serviceName");
4
4
  if (!trace) throw new Error("Missing trace");
@@ -44,7 +44,7 @@ function generateLowerHex(byteLength) {
44
44
 
45
45
  }
46
46
 
47
- export function toTraceAttributes(obj) {
47
+ function toTraceAttributes(obj) {
48
48
 
49
49
  const attributeMap = {};
50
50
  for (const [key, value] of Object.entries(obj)) {
@@ -81,8 +81,6 @@ async function writeLogLine({ log, projectId }, { message, tracePath, spanId, se
81
81
  traceSampled: "true",
82
82
  timestamp
83
83
  };
84
- console.log("Log meta", logMeta);
85
-
86
84
  const entry = log.entry(
87
85
  logMeta,
88
86
  {